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.
Tak, drodzy czytelnicy, ta struktura opisuje pola pewnej szczególnej macierzy, a mianowicie macierzy postaci takiej:
[1]
gdzie:
eM11 - składowa obrotu równa cos(α);
eM12 - składowa obrotu równa -sin(α);
eM21 - składowa obrotu równa sin(α);
eM22 - składowa obrotu równa cos(α);
eDx - przemieszczenie w osi x;
eDy - przemieszczenie w osi y.
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:
[2]
Mnożenie naszego punktu P przez macierz transformacji Mtr będzie wyglądać następująco:
[3]
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.
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ę:
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:
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:
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:
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.