[C#] DataGridViewColumn ContextMenu


(Marcin Obala) #1

Witam

Chciałbym umieścić checkbox jako nagłówek kolumny CheckBoxColumn w swoim DataGridView. Na początek poszukałem w internecie trochę i znalazłem kod który jest podany na bardzo wielu stronach jednak jak dla mnie jest on nieprawidłowy. Dlatego chciałbym zapytać co o nim sądzicie, co to w ogóle za konstrukcja for else. Poniżej kod

class DataGridViewCheckBoxColumnHeaderCell : DataGridViewColumnHeaderCell

{

    private Bitmap buffer;

    private CheckBox checkBox;

    private Rectangle checkBoxBounds;


    public DataGridViewCheckBoxColumnHeaderCell()

    {

        this.checkBox = new CheckBox();

    }


    public event EventHandler CheckedChanged;


    public bool Checked

    {

        get 

        { 

            return this.checkBox.Checked; 

        }


        set

        {

            if (!this.Checked == value)

            {

                this.checkBox.Checked = value;

                if (this.buffer != null)

                {

                    this.buffer.Dispose();

                    this.buffer = null;

                }


                this.OnCheckedChanged(EventArgs.Empty);


                if (this.DataGridView != null)

                {

                    this.DataGridView.Refresh();

                }

            }

        }

    }


    protected override void Paint(

        Graphics graphics, 

        Rectangle clipBounds, 

        Rectangle cellBounds, 

        int rowIndex, 

        DataGridViewElementStates dataGridViewElementState, 

        object value, 

        object formattedValue, 

        string errorText, 

        DataGridViewCellStyle cellStyle, 

        DataGridViewAdvancedBorderStyle advancedBorderStyle, 

        DataGridViewPaintParts paintParts)

    {

        // Passing String.Empty in place of 

        // value and formattedValue prevents 

        // this header cell from having text.


        base.Paint(

            graphics, 

            clipBounds, 

            cellBounds, 

            rowIndex, 

            dataGridViewElementState, 

            String.Empty, 

            String.Empty, 

            errorText, 

            cellStyle, 

            advancedBorderStyle, 

            paintParts);


        if (this.buffer == null 

            || cellBounds.Width != this.buffer.Width

            || cellBounds.Height != this.buffer.Height)

        {

            this.UpdateBuffer(cellBounds.Size);

        }


        graphics.DrawImage(this.buffer, cellBounds.Location);

    }


    protected override Size GetPreferredSize(

        Graphics graphics, 

        DataGridViewCellStyle cellStyle, 

        int rowIndex, 

        Size constraintSize)

    {

        return this.checkBox.GetPreferredSize(constraintSize);

    }


    protected override void OnMouseClick(DataGridViewCellMouseEventArgs e)

    {

        if (e.Button == MouseButtons.Left 

            && this.checkBoxBounds.Contains(e.Location))

        {

            this.Checked = !this.Checked;

        }


        base.OnMouseClick(e);

    }


    private void UpdateBuffer(Size size)

    {

        Bitmap updatedBuffer = new Bitmap(size.Width, size.Height);


        this.checkBox.Size = size;


        if (this.checkBox.Size.Width > 0 && this.checkBox.Size.Height > 0)

        {

            Bitmap renderedCheckbox = new Bitmap(

                this.checkBox.Width, 

                this.checkBox.Height);


            this.checkBox.DrawToBitmap(

                renderedCheckbox, 

                new Rectangle(new Point(), this.checkBox.Size));


            MakeTransparent(renderedCheckbox, this.checkBox.BackColor);

            Bitmap croppedRenderedCheckbox = AutoCrop(

                renderedCheckbox, 

                Color.Transparent);


            // TODO implement alignment, right now it is always

            // MiddleCenter regardless of this.Style.Alignment


            this.checkBox.Location = new Point(

                (updatedBuffer.Width - croppedRenderedCheckbox.Width) / 2, 

                (updatedBuffer.Height - croppedRenderedCheckbox.Height) / 2);


            Graphics updatedBufferGraphics = Graphics.FromImage(updatedBuffer);

            updatedBufferGraphics.DrawImage(

                croppedRenderedCheckbox, 

                this.checkBox.Location);


            this.checkBoxBounds = new Rectangle(

                this.checkBox.Location, 

                croppedRenderedCheckbox.Size);


            renderedCheckbox.Dispose();

            croppedRenderedCheckbox.Dispose();

        }


        if (this.buffer != null)

        {

            this.buffer.Dispose();

        }


        this.buffer = updatedBuffer;

    }


    protected virtual void OnCheckedChanged(EventArgs e)

    {

        EventHandler handler = this.CheckedChanged;

        if (handler != null)

        {

            handler(this, e);

        }

    }


    // The methods below are helper methods for manipulating Bitmaps


    private static void MakeTransparent(Bitmap bitmap, Color transparencyMask)

    {

        int transparencyMaskArgb = transparencyMask.ToArgb();

        int transparentArgb = Color.Transparent.ToArgb();


        List deadColumns = new List();


        for (int x = 0; x = 0; x--)

        {

            if (deadColumns.Count == bitmap.Height)

            {

                break;

            }


            for (int y = bitmap.Height - 1; y >= 0; y--)

            {

                if (deadColumns.Contains(y))

                {

                    continue;

                }


                int pixel = bitmap.GetPixel(x, y).ToArgb();


                if (pixel == transparencyMaskArgb)

                {

                    bitmap.SetPixel(x, y, Color.Transparent);

                }

                else if (pixel != transparentArgb)

                {

                    deadColumns.Add(y);

                    break;

                }

            }

        }

    }


    public static Bitmap AutoCrop(Bitmap bitmap, Color backgroundColor)

    {

        Size croppedSize = bitmap.Size;

        Point cropOrigin = new Point();

        int backgroundColorToArgb = backgroundColor.ToArgb();


        for (int x = bitmap.Width - 1; x >= 0; x--)

        {

            bool allPixelsAreBackgroundColor = true;

            for (int y = bitmap.Height - 1; y >= 0; y--)

            {

                if (bitmap.GetPixel(x, y).ToArgb() != backgroundColorToArgb)

                {

                    allPixelsAreBackgroundColor = false;

                    break;

                }

            }


            if (allPixelsAreBackgroundColor)

            {

                croppedSize.Width--;

            }

            else

            {

                break;

            }

        }


        for (int y = bitmap.Height - 1; y >= 0; y--)

        {

            bool allPixelsAreBackgroundColor = true;

            for (int x = bitmap.Width - 1; x >= 0; x--)

            {

                if (bitmap.GetPixel(x, y).ToArgb() != backgroundColorToArgb)

                {

                    allPixelsAreBackgroundColor = false;

                    break;

                }

            }


            if (allPixelsAreBackgroundColor)

            {

                croppedSize.Height--;

            }

            else

            {

                break;

            }

        }


        for (int x = 0; x = 0 && xWhole = 0)

                {

                    bitmapSection.SetPixel(x, y, bitmap.GetPixel(xWhole, yWhole));

                }

                else

                {

                    bitmapSection.SetPixel(x, y, Color.Transparent);

                }

            }

        }


        return bitmapSection;

    }

}

(Tomek Matz) #2

Taka instrukcja, tj. for else nie istnieje. Poszukałem jej w kodzie, który podałeś i niezgadzające się nawiasy klamrowe wskazują na to, że czegoś tam brakuje. Być może dwóch if-ów, a być może zagnieżdżonego for-a i jednego if-a. Reszty kodu nie oglądałem, bo nie widzę sensu ustawiania checbox-a jako nagłówek kolumny. Nigdy z takim czymś się nie spotkałem.


(slepcu) #3

nie widzisz sensu umieszkania checkboxa w naglowku kolumny zeby zaznaczyc wszystkie checkboxy w innych wierszach tej samej kolumny ?


(Tomek Matz) #4

Słuszna uwaga, ale nie sądzisz, że w ten sposób rozjedzie się cały layout DataGridView? Jak to będzie wyglądać. W nagłówkach pozostałych kolumn będzie sam tekst, a w tej jednej jakiś osamotniony CheckBox? Nie lepiej umieścić to w innym miejscu, np. w menu kontekstowym nagłówka kolumny lub poza samym DataGridView?. Można by też np. do nagłówka kolumny dodać taki mały (ale rzucający się w oczy) guziczek, który po naciśnięciu wyświetlałby kontrolkę (która składałaby się z kontrolki checkbox/combobox/textbox (w zależności od typu kolumny) i przycisku ok lub cancel) pod nagłówkiem kolumny.


(Marcin Obala) #5

Matzu. Wszystko ok, ja po prostu wpadłem, że chce coś takiego mieć i zacząłem szukać rozwiązań. Pamiętasz jak mi mówiłeś że po co zmieniać dataSource w kolumnach datagridview jak można dodać comboboxy poza dataGridView, nazwałeś to Master/Detail i właśnie z tego skorzystałem.

Teraz widzę problem tego typu że nawet jeśli zrobiłbym tak to trzeba utworzyć funkcje pod resize datagridview, resize columns, czy też resize form żeby ten CheckBox pływał i utrzymywał swoją pozycję i zmniejszał/zwiększał się tak jak to robi sama kolumna. Myślę po prostu żeby dodać contextMenu i opcję zaznacz wszystkie. Tak chyba najprościej będzie.


(Tomek Matz) #6

Dokładnie tak, menu kontekstowe to najprostsze rozwiązanie. Możesz je przypisać do rekordu w kolumnie lub do nagłówka kolumny. W tym pierwszym przypadku można skorzystać z designer-a. Należy wybrać właściwość Columns kontrolki DataGridView, a potem właściwość ContextMenuStrip konkretnej kolumny. W tym drugim przypadku przypisanie można wykonać tylko w kodzie, np. dataGridView1.Columns[0].HeaderCell.ContextMenuStrip = contextMenuStrip1; Wydaje mi się, że do Twojego problemu najlepszy będzie ten drugi przypadek ze względu na globalny charakter zmian, ale równie dobrze możesz użyć pierwszego przypadku i wybrać właściwą nazwę etykietę.


(Marcin Obala) #7

Ja i tak mam menu kontekstowe. Tzn. próbuje mieć bo mi ono nie wychodzi. Akurat to nie odnosi się do tematu ale napisze. Mam datagridview i na nim mam ContextMenu. Jednak nie do końca działa tak jakbym chciał. Chciałbym odczytać który wiersz jest pod miejscem w które kliknąłem. Wymyśliłem że zrobię to na zasadzie mouseClick i jeśli Button == MouseButton.Right to wyświetlam contextMenu i zapisuje miejsce kliknięcia w jakiś zmiennych po to żeby metody poszczególnych opcji contextMenu wiedziały co ostatnie było kliknięte. I teraz jest problem, jak odczytać z dataGridView wiersz mając koordynaty?

edit:

Teraz okazuje się że contextMenu za nic w świecie nie daje sobie wmówić że ma się wyświetlić w danym miejscu...


(Tomek Matz) #8

Najpierw przypisz menu kontekstowe do konkretnej kolumny. W tym celu wykonaj następujące kroki:

Następnie w kodzie strony oprogramuj zdarzenia CellMouseDown (DataGridView) oraz Click (ToolStripMenuItem) w następujący sposób:

private void dataGridView1_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)

{

    if (e.RowIndex != -1 e.ColumnIndex != -1)

        dataGridView1.CurrentCell = dataGridView1.Rows[e.RowIndex].Cells[e.ColumnIndex];

}


private void checkAllToolStripMenuItem_Click(object sender, EventArgs e)

{

    object value = dataGridView1.CurrentCell.Value;

    if (value != null)

    {

        MessageBox.Show(string.Format("row {0}, column {1}, value {2}", 

            dataGridView1.CurrentCell.RowIndex.ToString(),

            dataGridView1.CurrentCell.ColumnIndex.ToString(),

            value.ToString()));

    }

}

Wydaje mi się, że o to Ci właśnie chodzi, a jeśli nie to napisz proszę dokładniej na czym polega problem :slight_smile:


(Marcin Obala) #9

Ok, zrobiłem podobnie jak nie tak samo. Kolumnę dodaję dynamicznie w kodzie i tam też dopisałem contextMenu. Wszystko działa ok, dla testu zrobiłem 4 pozycje, Zaznacz, Odznacz, Zaznacz wszystkie, Odznacz wszystkie. I teraz takie małe pytanko, póki klikam prawym przyciskiem myszy wszystko jest ok, jednak jak kliknę na którąś komórkę lewym przyciskiem myszy datagridview trochę głupieje. Tzn. niby się klika, ale teraz klikam kolejną inną prawym i daję zaznacz wszystkie i tutaj nagle ta komórka zaznaczona nadal jest widoczna jako niezaznaczona (w sensie niezaznaczony checkBox), dopiero kliknięcie na inną sprawia że tamta poprzednia się też zaznacza.

Zrobiłem takie małe obejście na szybko ale nie wiem czy to eleganckie rozwiązanie.

private void zaznaczToolStripMenuItem_Click(object sender, EventArgs e)

{

    if (dataGridView1.CurrentCell != null)

    {

        dataGridView1.CurrentCell.Value = true;

        var tmp = dataGridView1.CurrentCell;

        dataGridView1.CurrentCell = null;

        dataGridView1.CurrentCell = tmp;

    }

}

(Tomek Matz) #10

Problem wynika najprawdopodobniej z tego, że ta zaznaczona komórka jest w trybie edycji (trzeba by programowo zakończyć edycję, żeby zmiana była od razu widoczna). Ja generalnie edytowałbym bezpośrednio źródło danych. Poniżej przykład:

mh4ysp.png

private void checkAllToolStripMenuItem_Click(object sender, EventArgs e)

{

    DataTable table = (DataTable)dataGridView1.DataSource;

    if (table != null)

    {

        foreach (DataRow row in table.Rows)

            row[dataGridView1.CurrentCell.ColumnIndex] = true;

        dataGridView1.Refresh();

    }


    checkCurrentToolStripMenuItem_Click(null, null);

}


private void checkCurrentToolStripMenuItem_Click(object sender, EventArgs e)

{

    //TODO: 

}

W checkCurrentToolStripMenuItem_Click musisz określić jak zachować się w momencie, gdy użytkownik kliknie check all albo check current dla komórki, która nie została jeszcze dodana do źródła danych (taką komórkę zaznaczyłem na zrzucie ekranu).


(Marcin Obala) #11

Kolumna do zaznaczania nie istnieje w źródle danych, jest ona dodana ręcznie kiedy przechodzę w tryb edycji. Takie ułatwienie po prostu np. do usuwania wierszy.


(Tomek Matz) #12

Rozumiem, ale wiesz, że poszczególne wiersze można zaznaczać używając ctrl + lewy przycisk myszy (tak jak się zaznacza kilka plików w katalogu)? A całą tabelę można zaznaczyć klikając w lewą górną komórkę w DataGridView?

private void checkAllToolStripMenuItem_Click(object sender, EventArgs e)

{

    for (int i = 0; i < dataGridView1.Rows.Count - 1; i++)

        dataGridView1.Rows[i].Cells[dataGridView1.CurrentCell.ColumnIndex].Value = true;

    dataGridView1.EndEdit();


    checkCurrentToolStripMenuItem_Click(null, null);

}

Jeśli chodzi o "check current" to występuje podobny problem jak pisałem wyżej.


(Marcin Obala) #13

Tak wiem i wczoraj się nad tym zastanawiałem. Wyrzuciłem kawałek Twojego kodu ten dotyczący currentCell a raczej wsadziłem go w if-a razem z selectedcells z tym że to działa tak że jeśli mam zaznaczony więcej niż jeden wiersz to prawy przycisk mysz nie podświetla kolejnego a jedynie wyświetla menu kontekstowe.


(Tomek Matz) #14

To teraz nie rozumiem. To w końcu już działa, czy jednak nie?


(Marcin Obala) #15

Tak działa. Przy zaznaczaniu z CTRL czy SHIFT opcja Check zaznacza te zaznaczone. Check all zaznacza wszystkie.