[Visual 2015 C# Windows Forms] Dynamiczne tworzenie kontrolek - językiem początkującego

W tym miejscu zadajemy pytania na temat języka C#, dzielimy się swoją wiedzą, udzielamy wsparcia, rozwiązujemy problemy programistyczne.
Awatar użytkownika
danielos
Newb
Newb
Posty: 69
Rejestracja: sobota 02 sty 2016, 15:06
Lokalizacja: Pawłowice, Silesia.
Kontaktowanie:

[Visual 2015 C# Windows Forms] Dynamiczne tworzenie kontrolek - językiem początkującego

Postautor: danielos » niedziela 01 maja 2016, 20:14

Witam.

Tym razem chciałbym się z wami podzielić wiedzą dot. dynamicznego tworzenia kontrolek w C#, oraz ich obsługi. Opis kolejnych czynności (kodów w programie) opisywał będę "językiem początkującego", czyli tak jak Ja to rozumiem.
Od razu chciałbym przeprosić bardziej zaawansowanych czytelników za formę i niepoprawne nazewnictwo niektórych czynności - piszę tak aby początkująca osoba potrafiła zrozumieć co się dzieje w danej linijce kody. Jednak jeżeli popełniłem jakieś rażące błędy, proszę o zwrócenie mi uwagi.

Zaczynajmy!

Działający_program.jpg

Program, który powstanie będzie powodował dynamiczne utworzenie kontrolek - ilość tych kontrolek będzie zależna od tego ile użytkownik wpisze do odpowiedniego pola. Kontrolki, które zostaną utworzone w sposób dynamiczny (tzn. ilość nie jest zdefiniowana w momencie kompilacji programu - będą one tworzone w czasie działania programu) to:
  1. TextBox
  2. Button
  3. Label
Dodatkowo, program będzie umożliwiał wykonanie przepisania zawartości z TextBox do Label po wciśnięciu odpowiadającego im przycisku.

W programie zostaną użyte jeszcze 4 kontrolki - Label, TextBox, Button x2.

Wygląd okna formularza przedstawia się następująco:
Wyglad_formularza.jpg

Pod przyciskiem Zamknij znajduje się polecenie umożliwiające zamknięcie programu:

Kod: Zaznacz cały

Close();

Natomiast cały program który będzie tworzył nowe kontrolki będzie znajdował się pod przyciskiem Utwórz. Do TextBox'a będzie wpisywana ilość kompletów elementów jaka ma zostać utworzona.

Jednak przed przystąpieniem do pisania kodu zerknijmy jak Visual Studio tworzy komponenty (po co samemu się głowić jak mamy dostępne gotowe rozwiązania)- w tym celu przechodzimy do pliku Form1.Designer.cs (wybieramy go z eksploratora projektu który znajduje się po prawej stronie). Na samym końcu naszej głównej klasy mamy deklarację użytych komponentów:

Kod: Zaznacz cały

        private System.Windows.Forms.Button button1;
        private System.Windows.Forms.Button button2;
        private System.Windows.Forms.TextBox textBox1;
        private System.Windows.Forms.Label label1;

Dalej w pliku tym znajdujemy metodę (czyli funkcję z klasy naszego programu) InitializeComponent() - znajduje się ona przed deklaracją komponentów. Wewnątrz tej metody (może ona być zwinięta - jeżeli tak jest należy po lewej stronie nacisnąć + aby ją rozwinąć) znajduje się cały kod odpowiedzialny za tworzenie naszego formularza i tego co się w nim znajduje. Na początku widzimy deklarację nowych kontrolek - obiektów (instancji klasy) ale o tym za chwilę. Fragment tego kodu prezentuje się następująco:

Kod: Zaznacz cały

            this.button1 = new System.Windows.Forms.Button();
            this.button2 = new System.Windows.Forms.Button();
            this.textBox1 = new System.Windows.Forms.TextBox();
            this.label1 = new System.Windows.Forms.Label();

Widzimy tutaj wyrażenie - this (z ang. to, ten). Oznacza ono że odnosimy się do obecnej klasy, w której został wywołany (stosuje się go głównie po to, aby uniknąć np. zbieżności nazw zmiennych występujących w klasie i np. w funkcji/metodzie). Do nowej zmiennej, np. button1, przypisywany jest nowy obiekt Button(), a dokładnie tworzony (uruchamiany jest konstruktor tego obiektu, czyli naszej kontrolki) obiekt - rezerwowane jest miejsce w pamięci na wszystkie funkcje (metody) które są przypisane dla tej klasy (która opisuje daną kontrolkę) - wszystko dzięki użyciu new.

Dalej w kodzie widzimy konfigurację przycisku:

Kod: Zaznacz cały

            //
            // button1
            //
            this.button1.Location = new System.Drawing.Point(197, 12);
            this.button1.Name = "button1";
            this.button1.Size = new System.Drawing.Size(75, 23);
            this.button1.TabIndex = 0;
            this.button1.Text = "Zamknij";
            this.button1.UseVisualStyleBackColor = true;
            this.button1.Click += new System.EventHandler(this.button1_Click);

W pierwszej kolejności ustawiamy punkt w którym będzie utworzony nasz komponent. Tworzona jest lokalizacja obiektu w podanym punkcie. Współrzędne podawane są w następującej kolejności: X, Y.
Następnie podawana jest nazwa przycisku (wyjaśnione to zostanie w dalszej części), rozmiar (tworzony jest obiekt w sensie wizualnym), określany jest kolejny numer podczas przełączania między kontrolkami przy użyciu TAB. Później ustawiamy wyświetlany tekst, oraz określenie czy tło ma być rysowane wg styli wizualnych. Na samym końcu dodawana jest metody w przypadku wykonania kliknięcia - dla zdarzenia CLICK. Dodawana z tego względu, że może być wywołanych wiele metod.
Tworzony jest nowy uchwyt/odnośnik dla zdarzenia (EventHandler jest delegatem, czyli wskaźnikiem) - mówiąc prościej, do zdarzenia Click przypisywany jest wskaźnik funkcji która ma zostać wywołana (jako parametr przekazujemy funkcję/metodę która ma obsłużyć to zdarzenie)

Przechodzimy jeszcze pod koniec tej metody. W sektorze odpowiedzialnym za utworzenie formularza odnajdujemy następujące fragmenty kodu:

Kod: Zaznacz cały

            this.Controls.Add(this.label1);
            this.Controls.Add(this.textBox1);
            this.Controls.Add(this.button2);
            this.Controls.Add(this.button1);

Odpowiedzialne one są za dodanie naszych kontrolek do formularza, a dokładanie do kolekcji formularza, czyli takiej tablicy, która może w sposób dynamiczny mieć zmienianą wielkość - w kolekcji łatwiejszy sposób są dodawane i usuwane wartości. Wszystkie kontrolki, które tworzymy (czy to z poziomu projektanta formularza, czy dynamicznie) znajdują się w takiej kolekcji. Niekoniecznie musi być to kolekcja formularza, może znaleźć się również kolekcja tzw. kontenerów - GroupBox, Panel, itp.

Czyli już wiemy na czym polega utworzenie nowej kontrolki. Przedstawię teraz w punktach kolejność postępowania w przypadku tworzenia nowej kontrolki:
  1. Deklaracja nowej kontrolki - tworzymy nową zmienną (której typem jest kontrolka)
  2. Przypisanie do naszej zmiennej obiektu kontrolki (wywołanie konstruktora)
  3. Ustawienie położenia kontrolki, oraz jej rozmiaru
  4. Przypisanie jej nazwy
  5. Pozostała konfiguracja kontrolki
  6. Dodanie kontrolki do kolekcji np. formularza

W taki sposób będziemy tworzyć również w sposób dynamiczny nasze kontrolki.

Tworzenie dynamiczne kontrolek będzie wykonywane po podaniu liczby w textbox i naciśnięciu przycisku Utwórz. Kod, który znajduje się pod tym przyciskiem wygląda następująco:

Kod: Zaznacz cały

        private void button2_Click(object sender, EventArgs e)
        {
            int liczba_elementow = Convert.ToInt16(textBox1.Text); //Konwersja wpisanej wartości do textboxa na int

            for (int i = 0; i < liczba_elementow; i++)
            {
                //tworzymy nową kontrolkę - textbox, button, oraz label
                TextBox ntextbox = new TextBox();
                Button nbutton = new Button();
                Label nlabel = new Label();

                //umieszczanie na ekranie danej kontrolki
                ntextbox.Location = new System.Drawing.Point(12, 110 + 26 * i);
                nbutton.Location = new System.Drawing.Point(118, 108 + 26 * i);
                nlabel.Location = new Point(213, 113 + 26 * i);

                //określenie rozmiarów kontrolki
                ntextbox.Size = new System.Drawing.Size(100, 20);
                nbutton.Size = new System.Drawing.Size(75, 23);
                nlabel.Size = new Size(35, 13);

                //przypisanie nazwy danej kontrolce
                ntextbox.Name = "d_box_" + i.ToString();
                nbutton.Name = "d_button_" + i.ToString();
                nlabel.Name = "d_label_" + i.ToString();

                //konfiguracja danej kontrolki - wyswietlany tekst, ewentualne ustawienia
                nbutton.Text = "Przepisz";   //nazwa przycisku
                nlabel.AutoSize = true;      //ustawienie automatycznego dopasowania szerokości

                //ustawienie zdarzenia dla przycisków
                nbutton.Click += new System.EventHandler(Nbutton_Click);

                //dodanie kontrolek do formularza (do kolekcji - tablicy)
                Controls.Add(ntextbox);
                Controls.Add(nbutton);
                Controls.Add(nlabel);

            }
        }

Większość kodu została wyjaśniona w komentarzach, bądź omówiona wcześniej.
Można jednak zauważyć że dla określenia położenia i rozmiaru kontrolki użyłem innego sposobu przypisania parametrów. Chciałem w ten sposób pokazać, że dzięki zdefiniowaniu na początku przestrzeni nazw System.Drawing nie musimy za każdym razem pisać skąd dana metoda ma zostać zaczerpnięta. Przydaje się to jednak w tedy, gdy w różnych przestrzeniach (klasach) mamy podobno brzmiące metody - w tedy zastosowane pełnych nazw powoduje uniknięcia błędu (pełnych nazw, czyli z jakiej przestrzeni/klasy jest zaczerpnięta dana metoda/funkcja).

Jak to jest z tą nazwą tej kontrolki? Otóż gdy tworzymy dynamicznie np. przycisk w kodzie musimy jakoś nazwać nasz nowy obiekt np. nbutton, dla którego będziemy ustawiać parametry. Dla jednego przycisku nie byłoby problemu, ale dla większej ilości musielibyśmy utworzyć więcej zmiennych - tylko to się mija z dynamicznym tworzeniem kontrolek. Dlatego gdy tworzymy w sposób dynamiczny nową kontrolkę najpierw wykorzystujemy nazwę którą zdefiniowaliśmy w programie (tworzymy tylko jedną zmienną dla jednego typu - bo po co więcej!?), aby łatwiej się konfigurowało ten i następny komponent (tworzony jest taki wskaźnik na dany obiekt, który w danej chwili jest konfigurowany), a w kolejnych czynności zmieniana jest dynamicznie nazwa tego komponentu. Pod tą nazwą znajduje się adres (indeks w kolekcji/tabeli) który wskazuje na nasz obiekt (kontrolkę) który jest teraz konfigurowany, więc nazwa ta może być zmieniana - zmieniając nazwę nie zmieniamy adresu gdzie przechowywana jest nasza kontrolka. Trochę to jest pokręcone, ale dzięki parametrowi Name, twórcy tego komponentu umożliwili w łatwy sposób możliwość zmiany nazwy tej kontrolki w sposób dynamiczny. Po dodaniu kontrolki do kolekcji np. formularza, aby się do niej odwołać, należy wykorzystać nazwę, jaka została przypisana poprzez parametr Name.
Dla kontrolek utworzony w projektancie formularza, parametr Name raczej nie będzie zmieniany w sposób dynamiczny podczas działania programu - bo po co, jak i tak w dalszej części programu nazwa tej kontrolki zostanie niezmieniona i każda próba odwołania się do tej kontrolki (która będzie miała już zmienioną nazwę) może spowodować błąd programu.
Mam nadzieję, że opisałem to dość zrozumiale - jakby co to pisać.

Musimy jeszcze napisać funkcję/metodę która będzie wykonywana po naciśnięciu na przycisk. Umieścimy ją po obsłudze przycisku Utwórz. Będzie ona miała następująco postać:

Kod: Zaznacz cały

        private void Nbutton_Click(object sender, EventArgs e)
        {
            //odczytanie numeru przycisku który spowodował wywołanie tego zdarzenia
            string nr_kontrolki = (sender as Button).Name.Remove(0, 9);

            //tworzymy nowe obiekty - dokładnie uchwyty do istniejących obiektów (poprzez wyszukanie FIND)
            TextBox ptextbox = (TextBox)this.Controls.Find("d_box_" + nr_kontrolki, false).First();
            Label plabel = this.Controls.Find("d_label_" + nr_kontrolki, false).First() as Label;

            //przepisanie zawartości
            plabel.Text = ptextbox.Text;

            //usunięcie przycisku który spowodował to zdarzenie - usunięcie z kolekcji
            Controls.Remove(sender as Button);

        }

Również w tym przypadku zaczerpnąłem z gotowego rozwiązania, które daje nam Visual - typy jakie są przekazywane do tej metody, oraz typ jaki zwraca ta metoda.
Co jest przekazywane do tej metody:
Poprzez zmienną sender przekazywany jest typ object, który informuje użytkownika, czyli nas z którego komponentu nastąpiło wywołanie tego zdarzenia. Dzięki temu mamy możliwość odwołania się do wszystkich właściwości tego komponentu. Drugim parametrem jest EventArgs, który przechowuje informacje o zdarzeniu - prawdę mówiąc jeszcze dokładnie nie wiem do czego on może służyć więc pominę opis tego parametru.

Aby można było odwołać się do kontrolki, która wywołała to zdarzenie, należy wykorzystać operator as (jako, tak jak), który umożliwi konwersję - w wolnym tłumaczeniu obiekt sender traktujemy jako przycisk button (dla tego rozważanego przypadku). Dzięki temu możliwy jest dostęp do wszystkich właściwości kontrolki, która spowodowała wywołanie tego zdarzenia.

Wróćmy jeszcze na chwilę do poprzedniego kodu. Tam jako nazwę dla danej kontrolki użyłem specjalnie dłuższego tekstu, który zakończony jest kolejnym numer - który jest ten sam dla każdej grupy kontrolek (w tym przypadku grupa kontrolek dla mnie to label, button i textbox). Można było użyć samego numeru co by ułatwiło identyfikację która grupa została wywołana - ale chciałem wykorzystać i sprawdzić metody które znajdują się w klasie string.
Tak więc w pierwszej kolejności w obsłudze przycisku musimy odczytać która grupa jest wywołana. Dokonujemy tego poprzez wycięcia słowa "d_button_" które ma 9 znaków - użyjemy w tym celu metody Remove, do której należy przekazać indeks od którego ma zacząć usuwać znaki, oraz ilość znaków do usunięcia. Tak przygotowaną informację przepisujemy do zmiennej nr_kontrolki, która jest typu string - nie jest konieczna konwersja na int, z tego względu, że w dalszej części programu składane będą nazwy kolejnych kontrolek.

Następnie tworzone są dwa nowe obiekty - label i textbox. Do nazw tych obiektów przypisujemy kontrolki, które zostały utworzone przez nas w sposób dynamiczny. Aby tego dokonać należy wykorzystać metodę Find, która powoduje wyszukanie w danej kolekcji kontrolki o podanej nazwie. Drugim parametrem jest wartość typu bool, która określa czy mają zostać również wyszukane kontrolki tzw. potomne (w naszym przypadku nie, więc ustawiamy false). Metoda ta zwraca w postaci tabeli wszystkie odpowiadające kontrolki. Można w łatwy sposób poprzez metodę First(), zwrócić pierwszą wartość tej tablicy. Należy jeszcze przekonwertować na odpowiednią kontrolkę dany obiekt - tutaj przedstawiłem dwa sposoby: poprzez jawne rzutowani i z zastosowaniem as (as w przypadku gdy nie uda się przekonwertować danych nie spowoduje błędu, tylko wpisze do zmiennej null, czego nie można powiedzieć o innych sposobach konwersji typów).
Następnie są przepisywane dane z jednej kontrolki do drugiej.
Na samym końcu usuwany jest przycisk który nacisnęliśmy - jest on usuwany całkowicie z kolekcji i nie mamy do niego dostępu w żaden sposób.

Ustawiamy jeszcze dla formularza właściwość AutoScroll na true - dzięki czemu pojawią nam się paski przewijania w przypadku gdy kontrolki wyjdą poza nasz formularz. Zmiany tej dokonujemy w projektancie formularza we właściwościach naszego Form1.

Chyba to wszystko. Mam nadzieję, że udało mi się w prosty sposób wytłumaczyć niektóre aspekty dynamicznego tworzenia kontrolek. Możliwe, że czasami za bardzo zakręciłem opisem, za co przepraszam.

W przypadku jakichś błędów, czy pytań pisać. Każda forma krytyki będzie motywująca;)

Jeszcze w załączniku przesyłam cały program wraz z komentarzami, aby każdy mógł sprawdzić jak to działa.

P.S.
:idea: :?:
W przyszłości pewnie pojawią się jeszcze moje wpisy dot. pewnych zagadnień z C#, szczególnie tych zagadnień co nie są za dobrze opisane i wyjaśnione w internecie. Sam się uczę tego języka i wiem, że dla początkującego trudno jest rozgryźć jakiś problem, gdy się danego tematu czy programu nie rozumie do końca.
Więc tutaj takie pytanko - czy taka forma opisywania może być? Czy jednak "język początkującego" odpada. Jak uważacie?
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.
Można wszystko osiągnąć, wystarczy chcieć.

SuperGość
Uber Geek
Uber Geek
Posty: 2346
Rejestracja: piątek 04 wrz 2015, 09:03

Re: [Visual 2015 C# Windows Forms] Dynamiczne tworzenie kontrolek - językiem początkującego

Postautor: SuperGość » niedziela 01 maja 2016, 21:23

C# to dla nie zupełnie obca kraina ale ogromne uznanie za włożoną pracę!


Wróć do „Pisanie programów w C#”

Kto jest online

Użytkownicy przeglądający to forum: Obecnie na forum nie ma żadnego zarejestrowanego użytkownika i 2 gości