BETA
Aby się zalogować, najpiew wybierz portal.
Aby się zarejestrować, najpiew wybierz portal.
Podaj słowa kluczowe
Słowa kluczowe muszą mieć co najmniej 3 sąsiadujące znaki alfanumeryczne
Pole zawiera niedozwolone znaki

Baza wiedzy











Kółko i krzyżyk – budowanie własnej kontrolki

17-02-2004 12:54 | User 79453

Wstęp

Biblioteka Windows Forms zawiera sporo kontrolek, z których możemy stworzyć rozbudowany interfejs użytkownika, czasami potrzebujemy jednak czegoś specyficznego, jak np. prezentowanej w tym artykule planszy do gry. Przy bardziej zaawansowanych aplikacjach musimy też często poprawiać drobne niedociągnięcia projektantów tej biblioteki i rozszerzać kontrolki o pewne właściwości (przykładem tego jest RichTextBox, która nie zawiera bardzo przydatnej przy tworzeniu jakiegokolwiek edytora informacji o wierszu i kolumnie, w której znajduje się kursor). W takich przypadkach warto poświęcić trochę więcej czasu i stworzyć kontrolkę, którą później będziemy mogli dodać do paska narzędzi i używać przy projektowaniu interfejsu w Visual Studio .NET.

Co nasza kontrolka ma robić?

Pierwszy krok, czyli wymyślenie, co nasza kontrolka ma robić. W moim przypadku jest to plansza do gry w kółko i krzyżyk, która powinna spełniać następujące wymagania:

  1. Plansza będzie tablicą pól, z których każde może być puste, zawierać kółko albo zawierać krzyżyk.

  2. Jeżeli w rzędzie, kolumnie lub po przekątnej znajdzie się pewna liczba kółek lub krzyżyków obok siebie, musi istnieć metoda zaznaczenia ich kreską oznaczającą wygraną.

  3. Kontrolka będzie służyła jedynie do wyświetlania informacji (nie będzie więc zawierała żadnych zasad gry), a informacje o ruchach użytkownika będzie przekazywała przez zdarzenia.

  4. Kontrolka będzie umożliwiała użytkownikowi konfigurowanie wszystkich elementów wyglądu, a więc także wysokości i szerokości pól i bitmap reprezentujących kółka i krzyżyki.

  5. Kontrolka musi być łatwa do wykorzystania w IDE Visual Studio, czyli powinna zawierać sensownie pogrupowane właściwości, ich opisy oraz ikonę dla paska narzędzi.

Od czego zacząć (a raczej z czego dziedziczyć)?

W dokumentacji MSDN w artykule “Authoring Controls for Windows Forms” przedstawiono trzy sposoby stworzenia własnej kontrolki:

  1. Stworzenie kontrolki dziedziczącej po innej, gotowej kontrolce biblioteki Windows Forms. Przykładem jest opisane wcześniej rozszerzenie RichTextBox-a o informacje o kolumnie i wierszu kursora. Nasza nowa kontrolka dziedziczy wygląd i wszystkie ustawienia, my dodajemy jedynie nowe własności.

  2. Stworzenie kontrolki dziedziczącej z klasy UserControl. Klasa ta jest czymś w rodzaju panelu, na którym możemy umieszczać inne kontrolki. Rozwiązanie to można zastosować przy stworzeniu kontrolki, która składa się z wielu innych. Przykładem może być klasa podobna do NumericUpDown, składająca się z pola edycji i dwóch małych przycisków, służących do zwiększenia i zmniejszenia wartości liczby z tego pola.

  3. Stworzenie kontrolki dziedziczącej bezpośrednio z klasy Control. Jest to najbardziej pracochłonne rozwiązanie, ponieważ musimy sami narysować kontrolkę, ale dające największą swobodę.

Moja plansza do gry nie przypomina żadnej istniejącej kontrolki, więc nie ma sensu rozwiązanie pierwsze. Nie składa się także z żadnych innych kontrolek (chyba, że zrobimy wielką tablicę obiektów klasy PictureBox, co jednak będzie rozwiązaniem nieefektywnym i nie ułatwiającym zbytnio pracy). Pozostaje więc dziedziczenie z klasy Control, czyli stworzenie kontrolki od podstaw.

Najtrudniejsze, czyli metoda OnPaint.

Nasza plansza do gry wywodzi się bezpośrednio z klasy Control, czyli musimy ją narysować sami. Robimy to przeciążając metodę OnPaint:

protected override void OnPaint(PaintEventArgs e)

{
     base.OnPaint(e);
...
}

Ta metoda jest wywoływana za każdym razem, gdy musimy odświeżyć zawartość naszej kontrolki (czyli wtedy, gdy zmieniliśmy jakieś dane i chcemy uaktualnić wygląd, wywołując metodę Invalidate, lub gdy system poinformuje, że nasza kontrolka musi być odświeżona – np. przy odsłonięciu jej fragmentu, dotychczas zasłanianego przez inne okno).

Jako argument tej metody jest przekazywany obiekt klasy PaintEventArgs, który zawiera dwie interesujące nas właściwości:

  1. Graphics. Jest to obiekt, którego będziemy używali do rysowania. Zawiera on metody w stylu DrawLine, DrawEllipse, FillRectangle, DrawImage i dziesiątki innych, dzięki którym w prosty sposób możemy narysować nawet bardzo skomplikowane elementy.

  2. ClipRectangle. Jest to prostokąt, którego zawartość musimy odrysować. Możemy go zignorować i za każdym razem odrysowywać całą zawartość kontrolki, co uprości nasz algorytm, jednak znacznie pogorszy szybkość działania.

Na początku wywołujemy metodę klasy bazowej. Pytanie – po co, skoro sama klasa Control nie ma żadnego wyglądu? Nie jest to do końca prawda – klasa Control ma właściwość BackColor, która określa kolor tła, oraz BackgroundImage, która określa obraz, który zostanie wyświetlony jako tło. I właśnie tło jest rysowane w Control.OnPaint. Drugim, powodem, dla którego musimy wywołać odpowiednią metodę klasy bazowej, jest wywołanie zarejestrowanych przez użytkownika funkcji obsługi zdarzenia Paint – są one wywoływane właśnie w OnPaint klasy Control. Jeżeli zapomnimy o wywołaniu base.OnPaint(e) (i nie wywołamy zdarzeń samemu), użytkownik naszej kontrolki nigdy nie zostanie poinformowany o konieczności odrysowania. Zasada ta dotyczy także przeciążania innych metod, jak np. OnMouseDown, OnMouseUp, ...

Problem:

W moim kodzie znalazło się coś takiego:

base.OnPaint (e);
     ...
// Wypełnianie odrysowywanego prostokąta
     grfx.FillRectangle(new SolidBrush(BackColor), e.ClipRectangle);

Nie ma to sensu – po pierwsze, wywołanie grfx.FillRectangle rysuje już wcześniej narysowane tło, po drugie – może zamazać wyświetlony obraz BackgroundImage.

Zanim przedstawię algorytm odrysowania planszy, powiem krótko, jak ona wygląda. Otóż plansza składa się z kolumn i wierszy, przedzielonych pojedynczymi liniami. Jak już pisałem, tło planszy jest takie, jak właściwość BackColor kontrolki, natomiast kolor linii – taki, jak ForeColor (właściwość ta także jest zdefiniowana w klasie Control). Każde z pól ma wysokość równą polu fieldHeight i szerokość równą polu fieldWidth (pola te możemy odczytywać i ustawiać przez właściwości o analogicznych nazwach, pisanych jednak z dużej litery – taką konwencję zastosowałem w całej klasie). Może być ono puste lub może zawierać jedną z dwóch bitmap – crossBmp lub noughtBmp (cross i nought wzięły się z angielskiej nazwy gry – Noughts and Crosses). Wielkości tych bitmap powinny być dostosowane do wielkości pola. Aby dodatkowo utrudnić sobie sprawę, na planszy może być jedna linia (nazwałem ją „linią zwycięzcy”, ponieważ powinna pokazywać leżące koło siebie kółka lub krzyżyki, oznaczające zwycięstwo), łącząca środki dwóch wybranych pól. Poniższy rysunek pokazuje przykładową planszę:

Algorytm odrysowywania planszy do gry wygląda następująco:

  1. Sprawdzamy, czy obszar, który musimy odrysować, przecina jakiekolwiek pole. Jeżeli nie, nie ma sensu jego odrysowywanie.

  2.      if ((e.ClipRectangle.Left > Columns * fieldWidth) ||
         (e.ClipRectangle.Top > Rows * fieldHeight))
              return;

  3. Wyznaczamy zakres pól, które musimy odrysować, i odrysowujemy je. Tutaj przedstawiam uproszczoną, ale wystarczająco efektywną wersję (skrajne linie mogą być rysowane nawet wtedy, gdy są już prawidłowo wyświetlane). Ponieważ za punkt honoru przyjąłem nie rysowanie niczego podwójnie, w dołączonym do artykułu kodzie źródłowym komponentu występuje trochę więcej warunków do sprawdzenia.

  4.      // Wyznaczanie zakresu pól do odrysowania
         int leftLine = (e.ClipRectangle.Left / fieldWidth);
         int topLine = (e.ClipRectangle.Top / fieldHeight);
         int rightLine = (e.ClipRectangle.Right / fieldWidth);
         int bottomLine = (e.ClipRectangle.Bottom / fieldHeight);
         bottomLine = Math.Min(bottomLine, Rows);
         rightLine = Math.Min(rightLine, Columns);
         ...
         using (grfx)
         using (linePen)
         {
         int left = e.ClipRectangle.Left;
         int top = e.ClipRectangle.Top;
              int right = Math.Min(e.ClipRectangle.Right,
         Columns * fieldWidth);
              int bottom = Math.Min(e.ClipRectangle.Bottom,
         Rows * fieldHeight);
          // Rysowanie linii pionowych
         for (int lx = leftLine; lx <= rightLine; lx++)
              {
                   grfx.DrawLine(linePen, lx * fieldWidth, top,
         lx * fieldWidth, bottom);
              }
         // Rysowanie liniii poziomych
              for (int ly = loopBegin; ly <= bottomLine; ly++)
         {
                   grfx.DrawLine(linePen, left, ly * fieldHeight,
                        right, ly * fieldHeight);
         }
              ...

    Wszystkie figury (a dokładniej ich brzeg) oraz linie rysujemy podając w odpowiedniej metodzie obiekt klasy Pen. Ja stworzyłem pióro podając jedynie kolor i szerokość, w rzeczywistości jednak ta klasa ma dużo więcej możliwości – możemy m.in. podać, w jaki sposób mają być łączone dwie stykające się linie, ustawić odpowiednie przekształcenie (dzięki czemu możemy np. stworzyć pióro o innej szerokości linii pionowych i poziomych), podać wygląd końców linii (np. zaokrąglenie, ścięcie), czy nawet ustawić odpowiedni wzór – np. linię przerywaną składającą się z powtarzających się dwóch kropek i kreski. Chyba najlepiej zagadnienia te są przedstawiony w książce „Programowanie Microsoft Windows w języku C#” (autor: Charles Petzold, ten sam, który napisał chyba najpopularniejszy podręcznik WinAPI), ale przy odrobinie chęci można bazować jedynie na dokumentacji MSDN.

    Być może część osób nie spotkała się wcześniej z użyciem słowa kluczowego using w takim miejscu. Konstrukcja:

    using (obj)
    {
    ...
    }

    dla obiektu implementującego interfejs IDisposable, oznacza to samo, co:

    try
    {
    ...
    }
    finally
    {
    obj.Dispose();
    }

    W moim programie zwalniam w ten sposób zasoby zajmowane przez obiekty Pen i Graphics.

  5. Rysujemy wszystkie pola. I tutaj, przy rysowaniu bitmap, pojawia się problem – jak zachować narysowane wcześniej tło? Rozwiązaniem jest zastosowanie obrazów z przezroczystym tłem (najpopularniejsze umożliwiające stosowanie przezroczystego tła formaty to GIF oraz PNG). Są one bez problemu obsługiwane przez GDI+ (czyli bibliotekę graficzną, z której korzystamy).

  6. Rysowanie bitmap odbywa się w metodzie RedrawField. Jest do tego wykorzystywane metoda DrawImage klasy Graphics, która automatycznie dopasowuje obraz do podanego prostokąta:

    if (board[x, y] == FieldType.Cross)
              grfx.DrawImage(crossBmp, left, top, fieldWidth - 1,
    fieldHeight - 1);
         else if (board[x, y] == FieldType.Nought)
         grfx.DrawImage(noughtBmp, left, top,
    fieldWidth - 1, fieldHeight - 1);

  7. Pozostaje jeszcze odrysowanie „linii zwycięzcy”, a raczej sprawdzenie, czy jest sens ją odrysowywać. Tutaj właśnie jest obiecywany we wstępie algorytm geometrii obliczeniowej – wykrycie, czy dana prosta przecina nasz odrysowywany obszar. Ponieważ obszar ten jest prostokątem, wystarczy sprawdzić, czy:

  8. a. Prostokąt ten zawiera któryś z końców linii, lub

    b. Linia przecina którykolwiek z boków prostokąta.

Pierwszy warunek możemy sprawdzić bardzo prosto – klasa Rectangle zawiera metodę Contains, która sprawdza, czy podany punkt zawiera się we wnętrzu prostokąta. Trochę trudniej sprawdzić drugą możliwość – pierwszym pomysłem jest najczęściej zabawa z sinusami i tangensami, ale jest ona dosyć żmudna i zmusza nas do operowania na liczbach zmiennoprzecinkowych, więc dużo łatwiej otrzymać złe wyniki dla pewnych skrajnych sytuacji. Dużo prostsze jest rozwiązanie bazujące na wektorach. Jego schemat, zaczerpnięty z książki „Wstęp do algorytmów” (autorzy: T. Cormen, Ch. Leiserson, R. Rivest), przedstawiam poniżej:

  1. Szybka eliminacja – odcinki mogą się przecinać tylko wtedy, gdy przecinają się prostokąty, w których się one zawierają. Najpierw tworzymy prostokąty zawierające obydwie linie (według zasady - lewy górny róg – najmniejsze współrzędne końców, prawy dolny – największe), następnie korzystamy z metody IntersectsWith klasy (a dokładniej struktury) Rectangle która mówi nam, czy dwa prostokąty się przecinają:

  2. Rectangle rect1 = new Rectangle(Math.Min(x1, x2),
    Math.Min(y1, y2),
    Math.Max(x1, x2) - Math.Min(x1, x2),
         Math.Max(y1, y2) - Math.Min(y1, y2));
    Rectangle rect2 = new Rectangle(Math.Min(x3, x4),
    Math.Min(y3, y4),
                   Math.Max(x3, x4) - Math.Min(x3, x4),
    Math.Max(y3, y4) - Math.Min(y3, y4));
    if (!rect1.IntersectsWith(rect2))
    continue;
    Jeżeli przeszliśmy pomyślnie szybką eliminację, musimy sprawdzić, czy każdy z odcinków przecina prostą zawierającą drugi. Jeżeli tak, odcinki się przecinają. Tylko jak to sprawdzić? Przyjmijmy, że pierwszy odcinek ma końce p1 i p2, natomiast drugi – p3 i p4. Warunek nasz będzie spełniony, jeżeli znak wartości iloczynów wektorowych (p3p1) x (p2 – p1) oraz (p4 – p1) x (p2 – p1) będą różne (lub przynajmniej jeden z nich będzie zerem). A wartość iloczynu wektorowego oblicza się już bardzo łatwo:

(p3p1) x (p2 – p1) = (x3x1)(y2y1) – (x2x1)(y3y1)

          // Teraz - sprawdzenie znaków odpowiednich iloczynów wektorowych
          int sgn1 = (x3 - x1) * (y2 - y1) - (x2 - x1) * (y3 - y1);
          int sgn2 = (x4 - x1) * (y2 - y1) - (x2 - x1) * (y4 - y1);
          if ((sgn1 * sgn2) <= 0)
               return true;
...
          

Wiem, że dla osób, które nigdy nie miały do czynienia z geometrią obliczeniową jest w tym rozwiązaniu sporo magii, ale po przeczytaniu trzech stron wspomnianej wyżej książki naprawdę wszystko staje się jasne.

Stosowanie powyższego algorytmu w przypadku prostej kontrolki nie ma może zbyt wiele sensu, ale sytuacja zmieni się, kiedy będziemy musieli odrysować nie jedną, lecz kilkanaście linii. GDI+ nie należy do demonów szybkości, więc przy większej ilości informacji warto trochę policzyć, aby zredukować liczbę rysowanych obiektów.

Urozmaicenie wyglądu, czyli podświetlanie pola, nad którym znajduje się kursor myszy.

W wielu programach element, nad którym znajduje się kursor myszy, jest w jakiś sposób zaznaczany. Dodanie tej właściwości do naszej kontrolki nie jest zbyt pracochłonne, a znacznie uatrakcyjnia jej wygląd. Jest z tym związane zagadnienie śledzenia położenia kursora myszy. Wykorzystamy do tego dwie metody wirtualne:

void OnMouseMove(MouseEventArgs e) – metoda wywoływana za każdym razem, gdy znajdujący się nad kontrolką kursor myszy zmieni położenie.

void OnMouseLeave(EventArgs e) – metoda wywoływana przy każdym wyjściu kursora myszy poza obszar kontrolki. Wykorzystane tej metody jest konieczne, ponieważ przy opuszczaniu obszaru kontrolki nie jest wywoływana metoda OnMouseMove, przez co ostatnio zaznaczone pole nadal pozostałoby aktywne.

Problem:

Na początku kod obsługi znajdował się także w trzeciej metodzie – OnMouseEnter. Nie ma to jednak sensu, ponieważ przy wejściu kursora myszy w obszar kontrolki jest wywoływana także metoda OnMouseMove.

Kod zaznaczający aktywne pole jest bardzo prosty – najpierw na podstawie współrzędnych kursora myszy wyznaczane jest odpowiednie pole (lub wykrywane, że kursor jest poza planszą), następnie jest ono porównywane z dotychczas aktywnym. Jeżeli są one różne, stare obramowanie jest odrysowywane kolorem określonym przez ForeColor, nowe jest rysowane kolorem SelectedFieldColor, a na końcu jest wywoływane funkcja sprawdzająca, czy musimy odrysować „linię zwycięzcy” (ta sama, którą opisałem przy metodzie OnPaint).

Wcześniej obiekt klasy Graphics, służący do rysowania, uzyskaliśmy jako argument. Ponieważ teraz nie korzystamy z OnPaint, musimy go otrzymać w inny sposób – wywołując metodę CreateGraphics klasy Control:

Graphics grfx = CreateGraphics();
...
int x = newSelected.X * fieldWidth;
int y = newSelected.Y * fieldHeight;
grfx.DrawRectangle(selectedPen, x, y, fieldWidth, fieldHeight);
...
int x = selectedField.X * fieldWidth;
int y = selectedField.Y * fieldHeight;
grfx.DrawRectangle(normalPen, x, y, fieldWidth, fieldHeight);

Pozostałe metody i właściwości.

Pozostałe metody i właściwości służą do ustawienia kolorów, grubości linii, rodzaju figur na poszczególnych polach planszy i „linii zwycięzcy”. Nie są one skomplikowane – większość z nich ustawia odpowiednie pole i wywołuje metodę Invalidate (która z kolei wywołuje metodę OnPaint). Najciekawszy jest chyba sposób powiadamiania użytkowników o kliknięciu lewym przyciskiem myszy na pole planszy. Jest to zrealizowane przez przeciążenie metody OnClick, która wykrywa, nad jakim polem znajduje się kursor, i wywołuje zdarzenie FieldClick, zdefiniowane następująco (starałem się dostosować do schematu nazewnictwa Windows Forms):

public delegate void FieldClickHandler(object sender, FieldEventArgs args);
public event FieldClickHandler FieldClick;

Ale pojawia się tutaj mały problem – skąd wiemy, którego przycisku użył użytkownik? Metoda OnClick otrzymuje jedynie obiekt ogólnej klasy EventArgs, która nie zawiera żadnych informacji o położeniu i naciśniętych przyciskach myszy. O ile z pozycją kursora możemy sobie poradzić, wywołując statyczną właściwość MousePosition klasy Control, to gorzej jest z wciśniętymi przyciskami. Wprawdzie klasa Control posiada także statyczną właściwość MouseButtons, która zwraca informacje o aktualnie wciśniętych przyciskach, ale my musimy to sprawdzić po kliknięciu, czyli w momencie, kiedy interesujący nas przycisk został zwolniony. Najlepsze rozwiązanie, na które wpadłem, polega na ustawieniu odpowiedniej flagi w metodzie OnMouseDown, która na szczęście dostaje zdecydowanie więcej informacji o stanie myszy.

protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown (e);
     if (e.Button == MouseButtons.Left)
          leftButtonDown = true;
     else
          leftButtonDown = false;
}

Uważam, że zdecydowanie łatwiejsze dla programisty byłoby przekazanie do metody OnClick bardziej szczegółowych informacji dotyczących myszy, tym bardziej, że są one bardzo często wykorzystywane.

     Problem:

Wspomniałem wcześniej, że klasa Control zawiera właściwość MousePosition, która zwraca pozycję myszy. Ale mój kod zamieniający tę pozycję na odpowiednie pole nie chciał początkowo działać. Później zorientowałem się, że pozycja ta jest podawana we współrzędnych ekranu, więc musimy ją jeszcze zamienić na współrzędne kontrolki, wywołując metodę (tym razem nie statyczną) PointToClient klasy Control.

Umieszczanie kontrolki w pasku narzędzi (Toolbox)

Mamy teraz już działającą kontrolkę, którą możemy użyć w programie, tworząc obiekt naszej klasy i dodając go w kodzie do formularza. Jest to jednak nieeleganckie i pracochłonne rozwiązanie – nie po to używamy Visual Studio, aby ręcznie budować cały interfejs użytkownika. Dodanie kontrolki do paska narzędzi nie jest skomplikowane, i w najprostszym przypadku sprowadza się do następujących czynności:

  1. Klikamy prawym przyciskiem na pasek narzędzi i z menu wybieramy opcję „Add / Remove Items” (tak w każdym razie jest w Visual Studio 2003. Według książki „Creating Windows-Based Applications with Visual Basic .NET and Visual C# .NET”, w poprzedniej wersji opcja ta nazywała się “Customize Toolbox”).

  2. W zakładce „.NET Frawework Components” klikamy na przycisk „Browse...” i wybieramy bibliotekę, która zawiera naszą kontrolkę (może to być zarówno plik .dll, jak i .exe). W naszym przypadku plik ten nosi nazwę Gameboard.dll.

  3. W tej samej zakładce na liście dostępnych komponentów zaznaczamy Gameboard.

I to wszystko – możemy teraz nasz komponent przeciągnąć na formularz, zmienić rozmiar, ustawić odpowiednie właściwości, takie jak tło, kolory linii, szerokość i wysokość komórek.

Możemy jeszcze dodać odpowiednią ikonę w pasku narzędzi i opisy specyficznych dla naszej kontrolki właściwości w oknie „Properties”. Aby je utworzyć, musimy skorzystać z atrybutów, czyli dodatkowych elementów opisujących naszą klasę. W rzeczywistości wszystkie atrybuty są obiektami klas rozszerzającymi klasę System.Attribute. Ich użycie najlepiej zilustruje przykład:

[Description("Szerokość pola na planszy"), Category("Appearance")]
     public int Columns
...

Dla właściwości Columns określiliśmy dwa atrybuty – Description, który oznacza opis właściwości pojawiający się w oknie „Properties”, oraz Category, czyli kategorię, do której nasza właściwość zostanie w tym oknie zaklasyfikowana. Reprezentującą naszą kontrolkę ikonę określamy przez atrybut ToolboxBitmap:

[ToolboxBitmap(typeof(Gameboard), "Resources.gameboard.bmp")]
public class Gameboard: Control
...

Podany obrazek musi być bitmapą o rozmiarach 16 na 16 pikseli. Musimy także określić w jakiś sposób, jaki plik nas interesuje. Możemy podać ścieżkę dostępu (ale przez to razem z naszą biblioteką będziemy musieli rozprowadzać osobny plik), lub też, tak jak powyżej, dodać naszą bitmapę do zasobów, określić, w jakim złożeniu (assembly) i przestrzeni nazw się ona znajduje oraz podać jej nazwę. Aby dodać bitmapę do zasobów, należy:

  1. W oknie „Solution Explorer” wybrać nasz projekt, następnie kliknąć prawym przyciskiem i wybrać opcję „Add Existing Item”.

  2. Wybrać interesujący nas plik.

  3. We właściwościach tego pliku jako „Build Action” ustawić „Embedded Resource”.

     Problem:

Ja miałem problemy z podaniem odpowiedniej nazwy do zasobów znajdujących się w moim programie – nie wiedziałem, w jakiej przestrzeni nazw się one znajdują. Przydatny okazał się program ildasm.exe, dzięki któremu można wyświetlić manifest naszej biblioteki i zobaczyć m.in. pełne nazwy dołączonych zasobów, oraz oczywiście dokumentacja MSDN.

Poniżej prezentuję wygląd okna właściwości naszej kontrolki:

Przykładowa aplikacja.

Do artykułu dołączam kody źródłowe stworzonej kontrolki oraz przykładowej aplikacji, umożliwiającej dwóm osobom grę w kółko i krzyżyk. Do uruchomienia wymaga ona .NET Framwork-a w wersji 1.1.

Załączniki:

Podobne artykuły

Komentarze 15

User 79543
User 79543
30 pkt.
Poczatkujacy
21-01-2010
oceń pozytywnie 0
Jeden z najleszpych artykułów.
j.gawinska3245
j.gawinska3245
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Ciekawy i wprowadza coś nowego (geometria obliczeniowa).
joanna.grzywna119
joanna.grzywna119
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Ciekawy artykuł.
mate3381
mate3381
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
krotko i na temat
karliks3326
karliks3326
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
ciekawy temat, w pelni zgadzam sie z moderatorem
User 79379
User 79379
9 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
świetny artykuł!!
wawelski_rulez3215
wawelski_rulez3215
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
ciekawy, ale te PROBLEMY...
ania_thompson3538
ania_thompson3538
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Przystępny język, logiczny tok wypowiedzi. Gratulacje!
jacekmalcher3537
jacekmalcher3537
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Podoba mi sie bardzo: koncepcja tekstu, orginalość, działający program. Choć problem przecinania odcinków jest zbyt matematyczny.
f_marh3540
f_marh3540
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
najs
toto10103145
toto10103145
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Najleszpy artykuł na tej stronie!!
zuraff3567
zuraff3567
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Zeby bylo wiecej takich artykulow.
kisielo.drimtim3578
kisielo.drimtim3578
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
jest okej.
pinky_ok3555
pinky_ok3555
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
prosty, zrozumiały, wypasiony
bazyliszek001
bazyliszek001
17 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Ciekawy artykuł tylko dlaczego tyle 1.
pkt.

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.