Autor podstrony: Krzysztof Zajączkowski

Stronę tą wyświetlono już: 3173 razy

Wstęp

Przydałoby się, aby nasze obiekty można było przemieszczać zaraz po ich zaznaczeniu. Warto się zastanowić jak powinna wyglądać obsługa takiego przemieszczania obiektów? Ja widzę to w dwóch różnych wersjach: za pomocą strzałek klawiatury i za pomocą myszki. Do zrealizowania pierwszego z nich trzeba będzie utworzyć specjalną grupę akceleratorów, pod które zostanie podpięta odpowiednia funkcja przesuwająca obiekty o określoną wartość. Do drugiego zaś zadania zaprzężona zostanie funkcja SetWorldTransform, która korzysta z wskaźnika na strukturę XFORM.

Macierz translacji i rotacji

Wiele osób naiwnie twierdzi, że w WinApi nie da się obracać w prosty sposób obiektów rysowanych. Nie jest to prawdą, gdyż właśnie wcześniej już wspominana funkcja SetWorldTransform oraz struktura XFORM, która z tą funkcją jest bezpośrednio powiązana umożliwiają obracanie wszystkich obiektów rysowanych w oknie programu. Co prawda na tej stronie nie będę omawiał obrotów, ale ogólne pojęcie użycia macierzy translacji i rotacji zostanie tutaj wyłuszczone.

Przyjrzyjmy się nieco bliżej strukturze XFORM:

typedef struct tagXFORM { /* xfrm */ FLOAT eM11; // komórka macierzy wiersz: 1, kolumna 1 FLOAT eM12; // komórka macierzy wiersz: 2, kolumna 1 FLOAT eM21; // komórka macierzy wiersz: 1, kolumna 2 FLOAT eM22; // komórka macierzy wiersz: 2, kolumna 2 FLOAT eDx; // komórka macierzy wiersz: 1, kolumna 3 FLOAT eDy; // komórka macierzy wiersz: 2, kolumna 3 } XFORM;

Tak, drodzy czytelnicy, ta struktura opisuje pola pewnej szczególnej macierzy, a mianowicie macierzy postaci takiej:

gdzie:

Jak to działa, załóżmy, że dany jest pewien 2W punkt P, pewien 2W punkt przemieszczenia Ptr oraz kąt α, gdzie mój punkt przemieszczenia jest punktem względem którego będę obracał dany punkt P. W takim to przypadku aby uzyskać wyjściową macierz transformacji trzeba wykonać następujące obliczenia macierzowe:

Mnożenie naszego punktu P przez macierz transformacji Mtr będzie wyglądać następująco:

W programie Rysowanie nie będę się tak mocno zagłębiał w transformacje wykorzystując strukturę XFORM do realizacji przemieszczania obiektów.

Obsługa przemieszczania obiektów za pomocą strzałek klawiatury

Dodawanie nowych akceleratorów

Na sam początek najprostsze, czyli dodanie kolejnych akceleratorów, które wykorzystać należy do obsługi przemieszczania obiektu o zadaną wartość. Na poniższej ilustracji pokazane zostały wszystkie niezbędne akceleratory.

Akceleratory programu Rysowanie
Rys. 1
Widok dodanych akceleratorów do projektu

Kod związany z obsługą nowych akceleratorów

W komunikacie WM_COMMAND należy zamieścić obsługę akceleratorów:

case WM_COMMAND: { if((HWND)lParam == toolbar){ UINT n = LOWORD(wParam); if(n != ID_SELECT && st == state::sel){ SelObjAct.Clear(); InvalidateRect(hWndDraw, NULL, true); } switch(n){ case ID_LINE: // komunikat przychodzący od przycisku toolbar-a o identyfikatorze ID_LINE { st = dr_line; SendMessage(statusbar, SB_SETTEXT, 2, (LPARAM)L"Rysowanie linii"); // ustawienie tekstu w ostatnim polu statusbar-u } break; case ID_CIRCLE: // to samo co poprzednio, tylko dla ID_CIRCLE { st = dr_circle; SendMessage(statusbar, SB_SETTEXT, 2, (LPARAM)L"Rysowanie okręgu"); // ustawienie tekstu w ostatnim polu statusbar-u } break; case ID_RECTANGLE: // to samo co poprzednio, tylko dla ID_RECTANGLE { st = dr_rect; SendMessage(statusbar, SB_SETTEXT, 2, (LPARAM)L"Rysowanie prostokąta"); // ustawienie tekstu w ostatnim polu statusbar-u } break; case ID_SELECT: // to samo co poprzednio, tylko dla ID_SELECT { st = sel; SendMessage(statusbar, SB_SETTEXT, 2, (LPARAM)L"Rysowanie edycja"); // ustawienie tekstu w ostatnim polu statusbar-u } break; } }else if(HIWORD(wParam) == 1){ // obsługa akceleratora if(st == state::sel){ switch(LOWORD(wParam)){ // id akceleratora case ID_ACCELERATOR_DELETE: // usuwanie obiektów { SelObjAct.Delete(hWndDraw, tObj); // wywołanie metody klasy SelObjAct, która usunie obiekty zaznaczone } break; case ID_ACCELERATOR_UP: // przemieszczanie w górę { POINT move = {0,- move_steep}; SelObjAct.Move(hWndDraw, move); } break; case ID_ACCELERATOR_DOWN: // przemieszczanie w dół { POINT move = {0, move_steep}; SelObjAct.Move(hWndDraw, move); } break; case ID_ACCELERATOR_LEFT: // przemieszczanie w lewo { POINT move = {- move_steep, 0}; SelObjAct.Move(hWndDraw, move); } break; case ID_ACCELERATOR_RIGHT: // przemieszczanie w prawo { POINT move = {move_steep, 0}; SelObjAct.Move(hWndDraw, move); } break; } } }

Z kolei w pliku select.h należy zamieścić zaraz po instrukcji #include stałą związaną z rozmiarem przesunięcia:

const int move_steep = 10;

Natomiast w klasie select_obj dodać nową następującą metodę:

void Move(HWND hWndDraw, POINT move){ if(!tSelObj.empty()){ if(selrect.left + move.x < 0) move.x = -selrect.left; if(selrect.top + move.y < 0) move.y = -selrect.top; OffsetRect(&selrect, move.x, move.y); // przemieszczanie prostokąta for(std::vector<i_dr_obj*>::iterator i = tSelObj.begin(); i < tSelObj.end(); i++){ // itaracja po zaznaczonych elementach (*i)->Move(move); // przemieszczanie } InvalidateRect(hWndDraw, NULL, false); // odświerzanie okna } }

Jak widać konieczne jest również wprowadzenie do wszystkich klas dziedziczących po klasie i_dr_obj metody Move. Dla klasy i_dr_obj:

virtual void Move(POINT move) =0; // przemieszczanie obiektu

Dla klasy line, circle oraz rectangle:

virtual void Move(POINT move){ begin.x += move.x; begin.y += move.y; end.x += move.x; end.y += move.y; }

Przemieszczanie obiektu poprzez wciśnięcie na zaznaczonym obiekcie klawisza myszki

Ponieważ do obsługi przemieszczania obiektów zaznaczonych posługiwał się będę strukturą XFORM, więc zacznę od kodu, który należy zamieścić przed definicją klasy i_dr_obj:

// ###################### MNOŻENIE MACIERZY TRANSFORMACJI ########################### XFORM operator * (const XFORM &xform, const XFORM &xform2){ XFORM f; f.eM11 = xform.eM11 * xform2.eM11 + xform.eM21 * xform2.eM12; f.eM21 = xform.eM11 * xform2.eM21 + xform.eM21 * xform2.eM22; f.eDx = xform.eM11 * xform2.eDx + xform.eM21 * xform2.eDy + xform.eDx; f.eM12 = xform.eM12 * xform2.eM11 + xform.eM22 * xform2.eM12; f.eM22 = xform.eM12 * xform2.eM21 + xform.eM22 * xform2.eM22; f.eDy = xform.eM12 * xform2.eDx + xform.eM22 * xform2.eDy + xform.eDy; return f; } // ##################### USTAWIANIE MACIERZY TRANSFORMACJI ########################### void SetXForm(XFORM &xf,float sinA,float cosA,float dx,float dy){ xf.eM11 = cosA; xf.eM12 = sinA; xf.eM21 = -sinA; xf.eM22 = cosA; xf.eDx = dx; xf.eDy = dy; }

Operator mnożenia obsługuje w powyższym kodzie mnożenie dwóch macierzy translacji i rotacji z kolei funkcja SetXForm ma za zadanie ustawiać pola struktury typu XFORM. Do klasy bazowej i_dr_obj należy dodać następujące dwa pola w części chronionej:

XFORM *moving; XFORM *wtransform;

Zmianie ulegnie również nagłówek konstruktora:

i_dr_obj(std::vector<hpen> &tPen, std::vector<hbrush> &tBrush, pen &p, brush &b, XFORM *wtransform):selected(false), pen_index(-1), brush_index(-1), moving(NULL),wtransform(wtransform)

Oraz konstruktorów klas pochodnych. Dla klasy line:

line(std::vector<hpen> &tPen, std::vector<hbrush> &tBrush, pen &p, brush &b,XFORM *wtransform):i_dr_obj(tPen, tBrush, p, b, wtransform){begin.x = begin.y = end.x = end.y = 0;};

Dla klasy circle:

circle(std::vector<hpen> &tPen, std::vector<hbrush> &tBrush, pen &p, brush &b, XFORM *wtransform):i_dr_obj(tPen, tBrush, p, b, wtransform){begin.x = begin.y = end.x = end.y = 0;};

Dla klasy rectangle:

rectangle(std::vector<hpen> &tPen, std::vector<hbrush> &tBrush, pen &p, brush &b, XFORM *wtransform):i_dr_obj(tPen, tBrush, p, b, wtransform){begin.x = begin.y = end.x = end.y = 0;};

Do klasy bazowej i_dr_obj należy dodać w części publicznej następujące dwie metody:

virtual void SetTransform() = 0; void SetMovingWorld(XFORM *moving){ this->moving = moving; }

Dla wszystkich klas pochodnych metoda czysto wirtualna SetTransform powinna wyglądać następująco:

virtual void Move(POINT move){ begin.x += move.x; begin.y += move.y; end.x += move.x; end.y += move.y; }

Ktoś może zarzucić mi, że powielam tutaj ten sam kod, jednakże ja ograniczam się w tej przykładowej implementacji programu jedynie do bardzo prostych obiektów, bardziej złożone obiekty nie będą miały tylko punktu początkowego i końcowego, ale mogą składać się z wielu punktów, i dlatego ten kod jest w tym przypadku powielany.

Pewnej zmianie ulegnąć musi metoda Draw, która dla klasy line będzie wyglądała teraz w następujący sposób:

virtual void Draw(HDC &hdc, std::vector<hpen> &tPen, std::vector<hbrush> &tBrush) const { if(moving){ XFORM tr = *moving * *wtransform; SetWorldTransform(hdc, &tr); } tPen[pen_index].SelectPen(hdc); tBrush[brush_index].SelectBrush(hdc); MoveToEx(hdc, begin.x, begin.y, NULL); LineTo(hdc, end.x, end.y); if(moving){ SetWorldTransform(hdc, wtransform); } }

Dla klasy circle:

virtual void Draw(HDC &hdc, std::vector<hpen> &tPen, std::vector<hbrush> &tBrush) const { if(moving){ XFORM tr = *moving * *wtransform; SetWorldTransform(hdc, &tr); } tPen[pen_index].SelectPen(hdc); tBrush[brush_index].SelectBrush(hdc); Ellipse(hdc, begin.x, begin.y, end.x, end.y); if(moving){ SetWorldTransform(hdc, wtransform); } }

I dla klasy rectangle:

virtual void Draw(HDC &hdc, std::vector<hpen> &tPen, std::vector<hbrush> &tBrush) const { if(moving){ XFORM tr = *moving * *wtransform; SetWorldTransform(hdc, &tr); } tPen[pen_index].SelectPen(hdc); tBrush[brush_index].SelectBrush(hdc); Rectangle(hdc, begin.x, begin.y, end.x, end.y); if(moving){ SetWorldTransform(hdc, wtransform); } }

Czas przejść do klasy select_obj, gdzie w sekcji chronionej zamieścić należy następujące trzy nowe pola:

XFORM translate; // macierz przemieszczenia bool move; // znacznik, informujący, że obiekt jest przemieszczany POINT lbMousePos; // pozycja myszki, gdy wciśnięty został lewy przycisk

Kolejne zmiany w kodzie metod klasy select_obj:

void WmLButtonDown(HWND hWndDraw, std::vector<i_dr_obj*> &tObj, WPARAM wParam, POINT mousepos){ // obsługa komunikatu wciśnięcia lewego przycisku myszki if(!(wParam & MK_SHIFT)){ // Gdy nie wciśnięto przycisku Shift na klawiaturze if(objundercursor){ // i pod mychą znajduje się jakiś obiekt translate.eDx = translate.eDy = 0; // zeruj pola związane z przemieszczaniem if(!objundercursor->Selected()){ // jak obietk nie jest zaznaczony to if(!tSelObj.empty()){ // jeżeli tablica zaznaczonych elementów nie jest pusta for(std::vector<i_dr_obj*>::iterator i = tSelObj.begin(); i < tSelObj.end(); i++){ // to iteruj po tej tablicy (*i)->Unselect(); // odznaczając je } tSelObj.clear(); // i wyczyść jej zawartość } objundercursor->Select(mousepos); // zaznacz niezaznaczony do tej pory obiekt selrect = objundercursor->GetRect(); // pobierz jego prostokąt tSelObj.push_back(objundercursor); // wstaw go do tablicy elementów zaznaczonych InvalidateRect(hWndDraw, NULL, false); // i odświerz okno } move = true; // ustawiam znacznik przemieszczania obiektu na true lbMousePos = mousepos; // zapamiętuję współrzędne kurosra przy wciśnięciu lewego przycisku myszy for(std::vector<i_dr_obj*>::iterator i = tSelObj.begin(); i < tSelObj.end(); i ++){ // iteruję po tablicy zaznaczonych obiektów (*i)->SetMovingWorld(&translate); // i ustawiam w nich wskaźnik na macierz przemieszczenia } return ; // kończę działania funkcji } if(!tSelObj.empty() && (!objundercursor || !objundercursor->Selected())){ // gdy tablica zaznaczonych elementów nie jest pusta i gdy żaden obiekt nie znajduje się pod kursorem myszy lub obiekt takowy istnieje ale nie jest zazaczony to for(std::vector<i_dr_obj*>::iterator i = tSelObj.begin(); i < tSelObj.end(); i ++){ // iteruj po wszystkich bierzących elementach kontenera zaznaczonych elementów (*i)->Unselect(); // odznacz wsystkie elementy } tSelObj.clear(); // i wyczyść tą tablicę } } if(objundercursor){ // gdy pod kursorem myszy znajduje się jakiś obiekt to if(objundercursor->Selected()){ // jeżeli ten obiekt jest zaznaczony if(wParam & MK_SHIFT){ // oraz przycisk shift jest wciśnięty to objundercursor->Unselect(); // odznacz tenże obiekt for(std::vector<i_dr_obj*>::iterator j = tSelObj.begin(); j < tSelObj.end(); j++){ // iteruj po kontenerze zaznaczonych obiektów if(objundercursor == *j){ // jeżeli dany element kontenera jest równy obiektowi znajdującemu się pod kursorem myszy to tSelObj.erase(j); // usuń ten element z kontenera break; // i zakończ iterację } } } }else{ // w przeciwnym przypadku tSelObj.push_back(objundercursor); // dodaj do tablicy zaznaczonych elementów obiekt znajdujący się pod kursorem myszy i objundercursor->Select(mousepos); // zaznacz go } }else{ // gdy żaden obiekt nie znajduje się pod kursorem myszki to for(std::vector<i_dr_obj*>::iterator i = tSelObj.begin(); i < tSelObj.end(); i ++){ // iteruj po wszystkich bierzących elementach kontenera zaznaczonych elementów (*i)->Unselect(); // odznacz wsystkie elementy } tSelObj.clear(); // i wyczyść kontener } if(!tSelObj.empty()){ // jeżeli kontenera zaznaczonych elementów nie jest pusta to selrect = tSelObj[0]->GetRect(); // pobieram prostokąt pierwszego elementu kontenera for(std::vector<i_dr_obj*>::iterator i = tSelObj.begin() + 1; i < tSelObj.end(); i++){ // iteruję po kolejnych elementach kontenera UnionRect(&selrect, &selrect, &(*i)->GetRect()); // i sumuję wszystkie jego elementy } }else{ // w przeciwnym przypadku SetRect(&selrect, mousepos.x, mousepos.y, mousepos.x, mousepos.y); // ustawiam pola struktury selrect na odpowiedające współrzędnym myszki } InvalidateRect(hWndDraw, NULL, true); // odświerzam okno rysowania } void WmLButtonUp(HWND hWndDraw, std::vector<i_dr_obj*> &tObj){ // obsługa komunikatu zwolnienia przycisku myszki if(move){ // jeżeli obiekt jest przemieszczany to move = false; // ustawiam znacznik przemieszczania na fałsz for(std::vector<i_dr_obj*>::iterator i = tSelObj.begin(); i < tSelObj.end(); i ++){ // iteruję po wszystkich elementach kontenera (*i)->SetTransform(); // ustawiając transformację (czyli w tym przypadku przemieszczenie obiektu } OffsetRect(&selrect, translate.eDx, translate.eDy); // przemieszczam prostokąt zaznaczenia o eDx i eDy return ; // kończę funkcję } if(tSelObj.empty()){ // jeżeli kontener zaznaczonych elementów nie jest pusty to SetRect(&selrect, min(selrect.right, selrect.left), min(selrect.top, selrect.bottom), max(selrect.right, selrect.left), max(selrect.top, selrect.bottom)); bool sel; for(int i = tObj.size() - 1; i > -1; i--){ // iteracja po elementach kontenera od tyłu sel = tObj[i]->Selected(); // pobieram informację, czy obiekt jest zaznaczony if(tObj[i]->Select(selrect)){ // jeżeli obiekt znajduje się w prostokącie if(sel){ // i był już wcześniej zaznaczony to tObj[i]->Unselect(); // odznacz go for(std::vector<i_dr_obj*>::iterator j = tSelObj.begin(); j < tSelObj.end(); j++){ if(tObj[i] == *j){ tSelObj.erase(j); break; } } }else{ tSelObj.push_back(tObj[i]); } } } if(!tSelObj.empty()){ selrect = tSelObj[0]->GetRect(); for(std::vector<i_dr_obj*>::iterator i = tSelObj.begin() + 1; i < tSelObj.end(); i++){ UnionRect(&selrect, &selrect, &(*i)->GetRect()); } }else{ SetRectEmpty(&selrect); } InvalidateRect(hWndDraw, NULL, true); } } void WmMouseMove(HWND hWndDraw, std::vector<i_dr_obj*> &tObj, WPARAM wParam, POINT &mousepos){ // obsługa ruchu myszki if(move){ // gdy obiekt jest przemieszczany to translate.eDx = mousepos.x - lbMousePos.x; // obliczam przemieszczenie na x-sie translate.eDy = mousepos.y - lbMousePos.y; // obliczam przemieszczanie na y-ku if(selrect.left + translate.eDx < 0) // sprawdzam, czy nie przekroczyłem minimalnego położenia na x-sie translate.eDx = -selrect.left; // poprawne wychylenie if(selrect.top + translate.eDy < 0) // sprawdzam, czy nie przekroczyłem minimalnego położenia na y-ku translate.eDy = -selrect.top; // poprawne wychylenie InvalidateRect(hWndDraw, NULL, false); // odświerzam } if(wParam & MK_LBUTTON){ if(tSelObj.empty()){ selrect.right = mousepos.x; selrect.bottom = mousepos.y; } InvalidateRect(hWndDraw, NULL, true); // odświerzanie okna w celu odrysowania nowych ustawień obiektu }else{ i_dr_obj* tmp = NULL; for(int i = tObj.size() - 1; i > -1; i--){ if(tObj[i]->CursorOnObject(mousepos)){ tmp = tObj[i]; break; } } if(objundercursor != tmp){ objundercursor = tmp; InvalidateRect(hWndDraw, false, true); } } } void WmPaint(HDC &hdc, XFORM *docLuw){ // obsługa rysowania XFORM tr = translate * *docLuw; if(move){ SetWorldTransform(hdc, &tr); } if(!tSelObj.empty()){ SelectObject(hdc, GetStockObject(NULL_BRUSH)); selframe.SelectPen(hdc); Rectangle(hdc, selrect.left, selrect.top, selrect.right, selrect.bottom); } selpen.SelectPen(hdc); selbrush.SelectBrush(hdc); for(std::vector<i_dr_obj*>::iterator i = tSelObj.begin(); i < tSelObj.end(); i++){ (*i)->DrawSelect(hdc); // rysowanie dodanych obiektów } if(tSelObj.empty() && selrect.right && selrect.bottom && selrect.top && selrect.left){ selrangepen.SelectPen(hdc); SelectObject(hdc, GetStockObject(NULL_BRUSH)); RECT r; SetRect(&r, min(selrect.right, selrect.left), min(selrect.top, selrect.bottom), max(selrect.right, selrect.left), max(selrect.top, selrect.bottom)); Rectangle(hdc, selrect.left, selrect.top, selrect.right, selrect.bottom); } if(objundercursor){ selbycursor.SelectPen(hdc); objundercursor->DrawSelect(hdc); } if(move){ SetWorldTransform(hdc, docLuw); } }

Przejdźmy teraz do pliku winmain.cpp, gdzie w funkcji WndDrawingProc najpierw utworzyć należy dwie dodatkowe zmienne statyczne typu XFORM opisujące transformację układów współrzędnych:

static XFORM guw = { // g - globalny; u - układ; w -współrzędnych 1.f, // cos(0) 0.f, // sin(0) 0.f, // -sin(0) 1.f, // cos(0) 0.f, // przemieszczenie x 0.f // przemieszczenie y }; static XFORM docLuw = {1.f, 0.f, 0.f, 1.f, 0.f, 0.f}; // l - lokalny; u - układ; w - współrzędnych

W komunikacie WM_CREATE trzeba ustawić dla kontekstu urządzenia mem odpowiedni tryb graficzny za pomocą funkcji SetGraphicsMode. Jest to konieczne, ponieważ bez tego program nie będzie w stanie wykonywać transformacji związanej z macierzą opisaną w strukturze XFROM.

case WM_CREATE: { obj = NULL; mem = CreateCompatibleDC(GetDC(NULL)); // tworzenie kompatybilnego z systemem kontekstu urządzenia SetGraphicsMode(mem, GM_ADVANCED); // ustawienie zaawansowanego trybu graficznego } break;

Zmiany w komunikacie WM_PAINT:

case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWndDraw, &ps); FillRect(mem, &wndRect, (HBRUSH)GetStockObject(BLACK_BRUSH)); // zamalowanie bitmapy na czarno SetWorldTransform(mem, &docLuw); // ustawienie układu współrzędnych związanego z dokumentem SetBkMode(mem, TRANSPARENT); for(std::vector<i_dr_obj*>::iterator i = tObj.begin(); i < tObj.end(); i++){ (*i)->Draw(mem, tPen, tBrush); // rysowanie dodanych obiektów } if(st == state::sel){ SelObjAct.WmPaint(mem, &docLuw); } if(obj){ obj->Draw(mem, tPen, tBrush); // rysowanie dodawanego obiektu } SetWorldTransform(mem, &guw); // ustawienie domyślnego układu współrzędnych BitBlt(hdc, 0, 0, wndRect.right, wndRect.bottom, mem, 0, 0, SRCCOPY); // przerysowanie bitmapy do okna EndPaint(hWndDraw, &ps); } break;

Również w komunikacie WM_LBUTTONDOWN zajdą pewne drobne zmiany:

case WM_LBUTTONDOWN: { SetCapture(hWndDraw); switch(st){ case dr_line: // tworzę obiekt typu line { obj = new line(tPen, tBrush, pen(255,0,0,1,pen::ps::solid), brush(0,0,150), &docLuw); obj->WmLButtonDown(mousepos); } break; case dr_circle: // tworzę obiekt typu circle { obj = new circle(tPen, tBrush, pen(0,255,0,1,pen::ps::solid), brush(0,150,0), &docLuw); obj->WmLButtonDown(mousepos); } break; case dr_rect: // tworzę obiekt typu rectangle { obj = new rectangle(tPen, tBrush, pen(0, 150, 255, 1, pen::ps::solid), brush(150,0,200), &docLuw); obj->WmLButtonDown(mousepos); } break; case sel: // trzyb zaznaczania (nie obsłużony jeszcze) { SelObjAct.WmLButtonDown(hWndDraw, tObj, wParam, mousepos); } break; } } break;

Po tym wszystkim mamy zrealizowaną obsługę przemieszczania zaznaczonych obiektów, które zostały dynamicznie utworzone.

Załączniki:

Wersja programu Rysowanie omawianej na tej stronie