The PolyDraw Explorer |
|
In creating my MetaFile Explorer, I needed to verify the actual representation of the EMR_POLYDRAW and EMR_POLYDRAW16 records. The documentation of these records was erroneous; it was clear that the documentation was essentially nonsensical simply by reading it.
So I did the PolyDraw Explorer just so I could create a little test case. It is not a very sophisticated program, but I thought I would make it available because it illustrates some document/view issues that are interesting to discuss, particularly in the presence of CFormView, and also with splitter windows.
The PolyDraw API takes two arrays: an array of points, and an array of byte flags that describe the meaning of the corresponding points. A point can be a MoveTo point, which starts a new polygon, a LineTo point, which draws a line from the previous endpoint to the new point, or a series of three BezierTo points which define the control points of a Bezier function.
BOOL PolyDraw(HDC hDC, const POINT * points, const BYTE * types, int count); BOOL CDC::PolyDraw(const POINT * points, const BYTE * types, int count);
The PolyDraw Explorer creates sample parameter lists and lets you see the effects.
The editor is rather trivial in this program, because I dashed it off in a hurry. Essentially, when you click on a line, its contents are transferred to the controls; modifying the controls modifies the contents of the active selection. It is a bit clumsy, but you have to Add an element to the list before you can edit it. The up and down arrow buttons move the current selection up and down in the list. When a point is selected in the list, it is shown in the view with a +. In the illustration below, the CloseFigure option has been deselected for the last BezierTo.
I wanted to have control over the layout. Had I had access to a decent grid control, such as the one at www.codeview.com, but I wrote this code in a hotel room in Ottawa, the Ottawa airport, on the plane to Washington DC, in the Washington DC airport, and on the flight back to Pittsburgh. None of these venues had functional connectivity to the Internet, so I ended up doing it this much cruder way. I decided I wanted to show the CloseFigure option as a check box. It seemed like a good idea at the time.
I also wanted the selection to highlight properly; for reasons I have never understood, the LVS_EX_FULLROWSELECT style seems to have no effect. I use a Wingdings check box character to display this. To create it, I need to change the LOGFONT fields as shown below.
case COL_CLOSEFIGURE: { /* closefigure */ if( pi->type & PT_CLOSEFIGURE) s = _T("\xFE"); // þ else s = _T("\x6F"); // ¨ LOGFONT lf; CFont * f = GetFont(); f->GetLogFont(&lf); lf.lfPitchAndFamily = FF_DECORATIVE; lf.lfCharSet = SYMBOL_CHARSET; StringCchCopy(lf.lfFaceName, sizeof(lf.lfFaceName)/sizeof(TCHAR), _T("Wingdings")); font.CreateFontIndirect(&lf); dc->SelectObject(&font); } /* closefigure */ break;
One of the problems of a CFormView-derived class is that the data is actually kept in the view, not in the document. There are several approaches you can take to solve this problem:
I prefer the latter, which sounds suspiciously like UpdateData (which I am not in favor of), the difference is that since UpdateData is limited in the number of things it can do, I avoid that trap. Also, I only capture the data when it is needed, not on every operation.
To do this, I use UpdateAllViews. I have an update code, UPDATE_DOCUMENT. When a view receives UPDATE_DOCUMENT. it transfers all its data to the document.
Because I was in a hurry, I did not bother with doing a fancy save, so I used CArchive
void CPolyDrawExplorerDoc::Serialize(CArchive & ar) { if (ar.IsStoring()) { UpdateAllViews(NULL, UPDATE_DOCUMENT, NULL); int size = GetCount(); ar.Write(&size, sizeof(size)); ar.Write(points.GetData(), size * sizeof(CPoint)); ar.Write(types.GetData(), size * sizeof(BYTE)); } else { int size; ar.Read(&size, sizeof(size)); SetDataSize(size); ar.Read(points.GetData(), size * sizeof(CPoint)); ar.Read(types.GetData(), size * sizeof(BYTE)); } }
Not the fanciest code, but note that on a Save/SaveAs operation, the Serialize function requests the data from the view. However, on an Open operation, it cannot send the data to the view, because typically the view has not yet been created. Therefore, it just stores the data in the document. The data is retrieved during view startup; for example,
void CPolyDrawControlView::OnInitialUpdate() { CFormView::OnInitialUpdate(); ... various initialization of controls DocumentToItems(); initialized = TRUE; updateControls(); } void CPolyDrawControlView::DocumentToItems() { int size = GetDocument()->GetCount(); if(size == 0) return; const CPoint * points = GetDocument()->GetPoints(); const LPBYTE types = GetDocument()->GetTypes(); for(int i = 0; i < size; i++) { /* set types */ PolyDrawItem * pi = new PolyDrawItem(points[i], types[i]); c_Data.InsertItem(pi); } /* set types */ } // CPolyDrawControlView::DocumentToItems
To handle the UPDATE_DOCUMENT, the OnUpdate handler is used. This also handles reporting errors from the CDocument-derived class and the CImageView class that is doing the drawing; these techniques will be discussed below. This is the handler for the CControlView, the left panel of the splitter window.
void CPolyDrawControlView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint) { UNREFERENCED_PARAMETER(pSender); if(lHint == 0 && pHint == NULL) return; switch(lHint) { /* lHint */ case CPolyDrawExplorerDoc::UPDATE_ERROR: ...see below return; case CPolyDrawExplorerDoc::UPDATE_DOCUMENT: ItemsToDocument(); return; } /* lHint */ }
The ItemsToDocument will store the items in the document.
void CPolyDrawControlView::ItemsToDocument() { GetDocument()->SetDataSize(c_Data.GetItemCount()); for(int i = 0; i < c_Data.GetItemCount(); i++) { /* scan items */ PolyDrawItem * pi = (PolyDrawItem *)c_Data.GetItemData(i); GetDocument()->SetData(i, pi); if(i == GetSelectedItem()) GetDocument()->SetActive(pi->pt); } /* scan items */ } // CPolyDrawControlView::ItemsToDocument
There are two views involved here; one is the control view, on the left, and the other is the drawing view, called CImageView, draws the image specified by the parameters in the left pane.
Therefore, it is necessary for the left pane to signal the right pane when there is a parameter change; there is a need for the right pane to obtain the parameters in a form usable by the PolyDraw API.
The document provides two methods that deliver the POINT array and the BYTE array required by the PolyDraw API:
const CPoint * GetPoints(); const LPBYTE GetTypes(); const int GetCount();
Thus, the API call in CImageView is just
BOOL b = pDC->PolyDraw(pDoc->GetPoints(), pDoc->GetTypes(), pDoc->GetCount());
To trigger a redraw, I just call UpdateAllViews with lHint==0 and pHint==NULL. So when there is a change in control state in the control view, it issues the call
GetDocument()->UpdateAllViews(this);
This will force the image view to update.
But what if the PolyDraw API fails, because it has an illegal parameter list, for example? I don't want obnoxious MessageBoxes popping up, but there's no convenient place the image view could display the error message. The sensible place to do this is in the control view. So I use inter-view communication to supply this information. The image view broadcasts out "I have an error", and any view that cares will handle this notification and display the error. It turns out the only view that cares is the control view, so the case for UpdateAllViews is in its OnUpdate handler is:
void CPolyDrawControlView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint) { UNREFERENCED_PARAMETER(pSender); if(lHint == 0 && pHint == NULL) return; switch(lHint) { /* lHint */ case CPolyDrawExplorerDoc::UPDATE_ERROR: { /* error */ DWORD err = (DWORD)(UINT_PTR)pHint; if(err == ERROR_SUCCESS) c_Status.SetWindowText(_T("")); else c_Status.SetWindowText(ErrorString(err)); } /* error */ return; ...see above
To move items up and down, the use of the owner-draw CListCtrl makes it very simple. All I have to do is swap the ItemData parts and invalidate the item rectangles. In the CPolyDrawControl class, I simply call the SwapItems method with n and n - 1 to move item n up, and with n and n + 1 to move item n down: To make nice-looking buttons, I use my CImageButton class.
void CPolyDrawControlView::SwapItems(int i1, int i2) { PolyDrawItem * d1 = (PolyDrawItem*)c_Data.GetItemData(i1); PolyDrawItem * d2 = (PolyDrawItem*)c_Data.GetItemData(i2); c_Data.SetItemData(i1, (DWORD_PTR)d2); c_Data.SetItemData(i2, (DWORD_PTR)d1); c_Data.Invalidate(i1); c_Data.Invalidate(i2); } // CPolyDrawControlView::SwapItems
The Invalidate method in the CPolyDrawCtrl handler will invalidate the item specified:
void CPolyDrawCtrl::Invalidate(int item) { CRect r; GetItemRect(item, &r, LVIR_BOUNDS); InvalidateRect(&r); } // CPolyDrawCtrl::Invalidate
I use the default Open, Close, Save and Save As handlers.
I added a new menu item, Write MetaFile..., to write a metafile. But the problem was: where do I put the handler? The possibilities are
It turns out that I chose to handle this message in the document. While it doesn't know how to draw a metafile, it does know how to notify whatever view that can draw the metafile that it should do so. To do this, I use UpdateAllViews, and in the CImageView::OnUpdate handler I route the message to the right place:
void CImageView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint) { switch(lHint) { /* lHint */ case 0: if(pHint == NULL) CScrollView::OnUpdate(pSender, lHint, pHint); return; case CPolyDrawExplorerDoc::UPDATE_METAFILE: WriteMetaFile(); return; } /* lHint */ }
Copy
To make the images for this Web page, I wanted to make a copy of the window image of the MDI frame. So the question was, where to handle this? Since I want to copy the frame, I decided the right place to put it is in the CChildFrame handler.
void CChildFrame::OnEditCopy() { toClipboard(this, TRUE); }
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.