The MetaFileExplorer |
|
A question came up on the newsgroup about metafiles. I had done a program quite a few years ago that worked with metafiles, but I'd never really completed it. In fact, when I went looking for it, I found that it had evaporated, probably in one of the numerous upgrades over the years (I try not to lose files, but it happens).
So I decided to rewrite it, and make it another exemplar of interesting programming techniques.
I did it as an MDI application, with a three-panel splitter. The left panel is a tree view that shows each of the metafile records. The middle panel controls the parameters (so much nicer than having to keep popping up a dialog box to set parameters!), and the rightmost panel is the actual image. Shown here are three images. The top and bottom images came from ordinary Windows Metafiles (WMF); the middle image (which is from the book Win32 Programming) is an enhanced metafile (EMF).
A metafile consists of a set of "metarecords" which are a replay of the graphics commands that created the image. A metafile is written by creating a metafile DC and executing the drawing commands using that DC. Metafile commands are essentially drawing commands and setup commands. These are distinguished by two different icons in the tree view. Setup commands are represented by a "wrench" icon, and drawing commands are indicated with a paper-and-pencil icon.
This shows the complete screen for a single image.
The leftmost panel shows the metafile records. The top-level view is just the metarecords themselves, such as EMR_HEADER, EMR_SAVEDC, EMR_POLYLINE, and so on (there are 122 kinds metarecords, of which several are reserved and are not actually defined).
Clicking on the [+] box of a metarecord expands it, and the fields are shown. Note that some fields are themselves expandable. Examining the above picture, note that the EMR_HEADER field contains all the header information. The offDescription field gives the offset from the header where the description string is found, and 88 bytes past the header is the string "Bezier Curves". The EMR_CREATEPEN record contains a LOGPEN structure, which has been expanded. The POLYBEZIER16 record contains a POINTS (16-bit point) array, and not surprisingly, the points shown are those which are later given as the point values in the illustration.
Not all metafile records are complete, since this is a work-under-construction, and not all metafile records have been tested. If you encounter a display problem, please let me know. In at least one case, the data appears to be erroneous; the RGNDATA data I found in some examples gives anomalously large values which cause access faults if used.
The control panel allows you to select how the information is displayed.
The top box is the Draw box, and it has three modes:
In the current implementation, the latter two are functionally identical (there was another intent during the design, that Single step drawing would single-step on a timer, but I have not implemented that yet).
In To Selection mode, the drawing is done only to the selected element in the tree. You can then use the arrow keys to move the selection up or down, seeing the effects. Note that many of the metarecords are state-management records, so moving move these one at a time is tedious; I added a second set of buttons, with the "draw" icon above them, which step to the next drawing record. The "To" part is inclusive, that is, it draws up to and including the selected record.
The Rectangle controls evolved after exploring several metafiles. The options are
All of these demonstrate different ways the PlayEnhMetaFile parameter can be set.
Windows Metafiles (.wmf) are less flexible than enhanced metafiles (.emf), but are still a staple of clip art. The program can read the older-style metafiles, but converts them internally to enhanced metafiles before trying to display them.
I wrote this as an extensible system. The metafile records are interpreted by using an abstract base class which decodes each record type and calls a corresponding virtual method. By creating a concrete subclass, I can implement anything I want. Currently, I have only implemented the display-as-tree module, but other modules are possible, including write-XML, write-C-code, write-Java-code (there's already a metafile-to-Java converter available), or pretty much any other mechanism is desired to transform an enhanced metafile to some alternative representation.
The tree converter consists of over a hundred methods of the form DecodeXXX where XXX is the name of the metafile record. Each Decode method takes the same parameters, which to allow for maximum flexibility, I have called DECODE_PARAMETERS. Currently, DECODE_PARAMETERS consists of a generic pointer to an ENHMETARECORD and two generic values which are LPVOID. For the tree converter, the first parameter is the CTreeCtrl * of the tree control and the second parameter is the HTREEITEM of the current item in the tree. But in an XML-writer, one of them could represent the current output stream (ostream *, FILE *, CStdioIoFile *, for example), and the second might not be needed at all.
To regularize the naming, I chose to use three different naming conventions based on the purpose of the function:
In addition, to make writing these easy, there are two kinds of representations for simple integer values: unique-element-from-a-set and one-or-more-of-the-following-flags values. I have a set of macros to handle these.
To handle the case of unique-element-from-a-set, the STRINGCASE/STRINGDEFAULT macros exist. They are used as
CString TreeDecode::cvtAC(DWORD dwAction) { switch(dwAction) { /* action */ STRINGCASE(CS_ENABLE); STRINGCASE(CS_DISABLE); STRINGCASE(CS_DELETE_TRANSFORM); STRINGDEFAULT(_T("%d"), dwAction); } /* action */ } // TreeDecode::cvtAC
If the action specified matches one of the cases, the string representing that action is returned. If none match, the STRINGDEFAULT macro formats the value in some appropriate way.
To handle the case of bit flags, the FLAG0/AddFlag/AddResiduum macros exist. They are used as
CString TreeDecode::cvtCAF(WORD caf) { FLAG0(caf); CString s; CString sep; AddFlag(s, sep, caf, CA_NEGATIVE); AddFlag(s, sep, caf, CA_LOG_FILTER); AddResiduum(s, sep, caf); return s; } // TreeDecode::cvtCAF
The FLAG0 macro determines if the value is 0 and simply returns the value "0". Otherwise, two CString variables are required, one to hold the currently-created string and one to hold the separator; the separator for AddFlag is always " | ". What AddFlag does is test to see if the specified flag is set; if it is, it is added to the first parameter, potentially separated by sep. The sep value is then set to be the appropriate separator. The bit is removed from the flag value (which can be a DWORD, UINT, WORD or BYTE). By the end of the sequence, all known bits should have been removed from the value; if not, there is a residual value, and the AddResiduum macro formats this as a hex value and adds it as if it were a field.
To add elements to the tree, the tree control and item are passed as the p1 and p2 parameters (the names are important, since the macro is written in terms of these names which are the parameter names from DECODE_PARAMETERS), so to render them usable, the GET_CTL_ITEM() macro declares CTreeCtrl * ctl and HTREEITEM item variables and sets them using the appropriate casts.
To format elements, the AddFormattedItem macro is used. Since it is a macro, the types of arguments would not be specified, and in any case there are requirements for "types" that don't exist in C/C++, but this captures the essence of it:
AddFormattedItem(CTreeCtrl * ctl, HTREEITEM item, class classname, LPVOID * object, class fieldtype, member fieldname, CString (*cvtfn)(fieldtype))
This calls the generic Decode function of the abstract superclass of all decode classes. This method is not virtual and is public. When called for decoding a tree, the first parameter is the ENHMETARECORD type, the second parameter (which is nominally an LPVOID) is the address of the tree control to add the elements to, and the third parameter, (which is nominally an LPVOID) is the item to add the field expansion to, which is the item just added to represent the record whose pointer is passed in.
void CMetafileExplorerView::DecodeRecord(HTREEITEM item, CONST ENHMETARECORD * rec) { CTreeCtrl & ctl = GetTreeCtrl(); TreeDecode d; d.Decode(rec, &ctl, (LPVOID)item); } // CMetafileExplorerView::DecodeRecord
One of the common questions that comes up with multiple views is "How do I inform another view that...?" The answer is UpdateAllViews. Because I wanted this to be nicely generalized. It also gave me an opportunity to exercise my fundamental laziness as a programmer by allowing distributed control and designation of what was going on.
The classic case of UpdateAllViews is to use a non-zero lHint value to indicate "what to do" and, if necessary, the pHint to pass information about what to do, or receive information from a query. The problem is, how to designate what the lHint values are. The classic approach is a set of #define constants, or const int declartions, or a typedef enum{...} of a set of enumeration constants. The problem with this method is that it centralizes the dependencies. If view A wants to communicate to view B, they must both import the set of definitions, and this will include view C communicating to view D. There is no way to have A and B exist completely independent of C and D. If A and B have no relationship to C and D, we have now introduced a meaningless interdependency. In fact, in my early implementation of this, I had only document-related messages, so it was easy. But once I started adding inter-view communication, I found the gratuitous coupling offensive.
So I thought about it for a while, and wondered how I could create non-conflicting values. The obvious solution was to use ::RegisterWindowMessage. This led immediately to the conclusion "so why not use window messages?" This was because I could no longer use a nice simple switch statement to decode the lHint, because switch requires compile-time constant values. Using if-statements seemed so yesterday (I'm obviously style-conscious...). Well, if I have a registered window message, why not use it?
I could have written a handler in my document subclass that iterated across all views and did a SendMessage, and perhaps in some future version I might do that, but what I did was simply use the existing UpdateAllViews method. My OnUpdate handler in each view will do a SendMessage (and not a PostMessage, because I want it to be synchronous). The generic pattern for all handlers is:
void CwhateverView::OnUpdate(CView* pSender, LPARAM lHint, CObject * pHint) { if(lHint == 0 && pHint == NULL) { /* just redraw */ CSuperclassView::OnUpdate(pSender, lHint, pHint); return; } /* just redraw */ SendMessage((UINT)lHint, (WPARAM)pHint); }
Now inter-view messaging is simple: the two views decide on what messages they want, and feel free to send them via UpdateAllViews. Any view that understands the message has a Message Map entry to decode it; no view that does not understand the message is impacted at all. New views and new messages can be added between specific views without any other view even requiring a recompilation.
This table summarizes the kinds of messages I send, who sends them, and the expected behavior. In the descriptions, the "Tree view" is the leftmost panel, the "Control view" is the middle panel, and the "Image view" is the rightmost panel.
Message (parameters) | Sent by | Target | Explanation |
EMF_FILE(LPCTSTR filename) | Document | All views | Tells the views what the file name is (as interpreted by the tree view, this creates the root node of the tree) |
EMF_RECORD(ENHMETARECORD * emr) | Document | All views | Tells the views that a metarecord has been found (as interpreted by the tree view, this creates a second-level node in the tree) |
IMG_INVALIDATE() | Document, any view | Image view | Tells the image view to invalidate itself because a change which requires repainting has occurred |
IMG_QUERY_RECT(CRect * r) | Any view | Image view | The image view provides the size of its client rectangle. |
QRY_ENABLE(QueryEnableInfo * info) | Control view | Tree view | Queries the types of moves that are permissible given the current selection |
QRY_RECT(CRect * r) | Any view | All views | Any view which wishes to supply a bounding
rectangle is free to do so Notes: The tree view provides the EMRHEADER.rclBounds if no one else has set the rectangle. The Control View will respond unconditionally if the user has chosen to override the metafile rectangle. |
QRY_SHOWREQUEST(QueryShowInfo * info) | Document | Tree view | Given the information provided as to which metarecord is about to be displayed, discovers if it should be displayed |
UPD_MOVE_UP(amount) | Control view | Tree view | Any view that is tracking a selection will move the selection up. The amount will be either UPDF_BY_LINE to move up or down one line, or UPDF_BY_DRAW to move up or down to the previous drawing object |
UPD_MOVE_DOWN(amount) | |||
UPD_SELECTION_CHANGED() | Tree view | All views | Indicates that a selection of a metarecord has changed |
UPD_UPDATECONTROLS() | Document | All views | Tells views that have controls that there has been a state change that requires an update |
Essentially, a view that is interested in something, or has something interesting happen that requires notification of other views, simply "broadcasts" the message to all views. A notification is handled only by the views that care. A query is handled by one or more views. One of the interesting aspects was the choice of QRY_RECT, where either the control view would receive it first, or the tree view would receive it first. So I "prioritized" the behavior; if the tree view receives it, and it was not filled in, it fills it in; if it was filled in, the tree view leaves it untouched. On the other hand, the control view always fills the value in, no matter what might be there. Thus the control view, if it is designated to supply the rectangle, will have priority.
I thought this would be easy. Just open a file, and use the ReadMetaFile (instead of the ReadEnhMetaFile) API. No, this doesn't work. It just returns a NULL handle, and unlike the documentation claims, ::GetLastError merely returns S_OK/ERROR_SUCCESS, which is not terribly informative.
I tried reading the raw data from the file and doing ::SetWinMetaFileBits, but that failed also, but this time at least gave a meaningful error: "Bad data". A bit of investigation revealed that a WMF file has a "placeable header" prolog. So I copied the structure from the documentation, offset the data I read from the file by that amount, and it worked!
To handle all this, I created a new kind of document class to read WMF files, CMetafileExplorerWMFDoc. But I didn't want to replicate all the code of the existing CMetafileExplorerDoc class, so I did the obvious (at least to me): I made the new class be a subclass of the existing class! Now all I had to do was use OnOpenDocument as the only function in the new class! A great way to be a truly lazy programmer! The result is that the handle to the metafile that is used is always stored in the superclass and it is always an HENHMETAFILE handle.
BOOL CMetafileExplorerWMFDoc::OnOpenDocument(LPCTSTR filename) { CFile f; if(!f.Open(filename, CFile::modeRead)) // [1] return FALSE; ULONGLONG size = f.GetLength(); // [2] // We will assume we have no metafiles > 4.2GB in length... CByteArray data; // [3] data.SetSize((UINT_PTR)size); // [4] f.Read(data.GetData(), (UINT)size); // [5] f.Close(); // [6] METAFILEPICT mfp = {MM_TEXT}; // [7] LPBYTE bits = data.GetData(); // [8] ASSERT(sizeof(PLACEABLEMETAHEADER) == 22); PLACEABLEMETAHEADER * hdr = (PLACEABLEMETAHEADER*)bits; // [9] if(hdr->Key != 0x9AC6CDD7) // [10] { /* not placeable header */ ASSERT(FALSE); // bad format? // TODO: something smarter here return FALSE; } /* not placeable header */ bits = bits + sizeof(PLACEABLEMETAHEADER); // [11] HENHMETAFILE enh = ::SetWinMetaFileBits((UINT)size, bits, NULL, &mfp); // [12] if(enh == NULL) // [13] return FALSE; meta = enh; ProcessMetafile(filename); // [14] return meta != NULL; // [15] } // CMetafileExplorerWMFDoc::OnOpenDocument
Open the file displayed
Obtain the length of the file
I don't believe in doing new or malloc because these require remembering the free the data. Using a class like CByteArray (or std::vector<BYTE>) means that I don't have to worry about deallocation; the buffer will be deallocated when the variable leaves scope.
I force the CByteArray to have a length large enough to hold the entire file
I read the data into the buffer. For CArray and friends, the GetData method returns a pointer to the buffer which represents the array
I no longer need the file, so I can close it
To call ::SetWinMetaFileBits, a METAFILEPICT structure is required. The first member is the mapping mode used, which I default to MM_TEXT
To get a pointer to the bits of the metafile, I first use GetData to get a pointer to the buffer
I set a pointer to a PLACEABLEMETAHEADER to point to the data buffer
If it is a placeable header, the "signature" in the Key field will be 0x9AC6CDD7
If there is a placeable header, offset by its length (which is 22 bytes) to the actual start of the metafile
Call ::SetWinMetaFileBits to convert the old-style WMF file to the new-style EMF file
If this fails, return FALSE
ProcessMetafile is defined in the superclass, and it will enumerate all the metafile records and load the tree control
If the meta variable is still non-NULL, the result will be TRUE
To make this work properly a new document template must be created. The original document template was
pDocTemplate = new CMultiDocTemplate(IDR_MetafileExploreTYPE, RUNTIME_CLASS(CMetafileExplorerDoc), RUNTIME_CLASS(CChildFrame), // custom MDI child frame RUNTIME_CLASS(CMetafileExplorerView)); if (!pDocTemplate) return FALSE; AddDocTemplate(pDocTemplate);
The second document template is
pDocTemplate = new CMultiDocTemplate(IDR_MetafileWMFType, RUNTIME_CLASS(CMetafileExplorerWMFDoc), RUNTIME_CLASS(CChildFrame), RUNTIME_CLASS(CMetafileExplorerView)); if (!pDocTemplate) return FALSE; AddDocTemplate(pDocTemplate);
The two changes are to specify a new ID for the template, and associate it with the new document type. Note that the child frame and view are the same as the original.
The ID identifies several associated components
The icon displayed for the system menu
The menu associated with the view
The document string
The document string is kept in the STRINGTABLE and contains several substrings which define the document type, and is generally hard to read because everything is all jammed together separated by \n characters, for example
\nMetafileExplore\nMetafileExplore\nEnhanced Metafile Files (*.emf)\n.emf\nMetafileExplorer.Document\nMetafileExplorer.Document
The new string was created by making a copy of the first string, pasting it back, and hand-editing the StringID, filterName, and filterExt fields.
\nMetafileExplore\nMetafileExplore\nMetafile Files (*.wmf)\n.wmf\nMetafileExplorer.Document\nMetafileExplorer.Document
Broken down into fields it is actually as shown in the following table (the names are from the DocStringIndex parameter of CDocTemplate::GetDocString
String ID | docName | fileNewName | filterName | filterExt | regFileTypeID | regFileTypeName | ||||||
IDR_MetafileExploreTYPE |
\n |
MetafileExplore |
\n |
MetafileExplore |
\n |
Enhanced Metafile Files (*.emf) |
\n |
.emf |
\n |
MetafileExplorer.Document |
\n |
MetafileExplorer.Document |
IDR_MetafileExploreWMFTYPE |
\n |
MetafileExplore |
\n |
MetafileExplore |
\n |
Metafile Files (*.wmf) |
\n |
.wmf |
\n |
MetafileExplorer.Document |
\n |
MetafileExplorer.Document |
By adding the document template the File > Open command will recognize the multiple extensions:
The ProcessMetafile method is straightforward
void CMetafileExplorerDoc::ProcessMetafile(LPCTSTR filename) { UpdateAllViews(NULL, EMF_FILE, (CObject *)filename); CRect r; ::EnumEnhMetaFile(NULL, meta, metaEnum, this, &r); UpdateAllViews(NULL, UPD_UPDATECONTROLS, NULL); } // CMetafileExplorerDoc::ProcessMetafile
The enumerate is done by a static method
/* static */ int CALLBACK CMetafileExplorerDoc::metaEnum(HDC, HANDLETABLE *, CONST ENHMETARECORD * rec, int, LPARAM p) { CMetafileExplorerDoc * doc = (CMetafileExplorerDoc *)p; doc->UpdateAllViews(NULL, EMF_RECORD, (CObject *)rec); return 1; // any nonzero value to continue enumeration } // CMetafileExplorerDoc::metaEnum
The LPARAM value allows us to move from the CALLBACK "C space" to the "C++ space". See my essay on callbacks.
The drawing is done by using ::EnumEnhMetaFile and a callback. The callback conditionally executes ::PlayEnhMetaRecord.
void CImageView::OnDraw(CDC * pDC) { HENHMETAFILE meta = GetDocument()->GetMetaFile(); // [1] CRect r; r.SetRectEmpty(); // [2] GetDocument()->SendViewMessage(QRY_RECT, &r); // [3] if(r.IsRectNull()) // [4] GetClientRect(&r); // [5] SetScrollSizes(MM_TEXT, CSize(max(0, r.Width()), max(0, r.Height()))); // [6] ::EnumEnhMetaFile(pDC->m_hDC, meta, DrawEnum, this, &r); // [7] } // CImageView::OnDraw
Obtain the handle to the HENHMETAFILE from the document
Set the rectangle to be (0,0,0,0)
Ask the views to supply a bounding rectangle
Ask if the rectangle is still (0,0,0,0)
If it is, no other view responded; set the rectangle to be the client rectangle
Set the scroll range based on the rectangle using CScrollView::SetScrollSizes
Enumerate the metafile using the HDC passed in as the parameter to OnDraw, the metafile handle, using the function DrawEnum. The CImageView instance is passed in, and the bounding rectangle is the rectangle obtained via the query, or the client rectangle if the quire failed to do an update
That's basically all there is to OnDraw! The DrawEnum function is
/* static */ int CALLBACK CImageView::DrawEnum(HDC dc, HANDLETABLE * handles, CONST ENHMETARECORD * record, int count, LPARAM p) { CImageView * me = (CImageView *)p; // [1] if(me->GetDocument()->QueryShow(record)) // [2] { /* show it */ ::PlayEnhMetaFileRecord(dc, handles, record, count); // [3] } /* show it */ return 1; // [4] } // CImageView::DrawEnum
Use the LPARAM parameter passed in at the call site to move from C space back to C++ space (see my essay on callbacks)
Ask the document to find out if we should show this record. It will send a QRY_SHOWREQUEST notification to all views, which will return a BOOL
If the value is nonzero, call ::PlayEnhMetaFileRecord
Return a nonzero value (note that the return type of an ::EnumMetaFile handler is, for reasons unknown and unknowable, an int, not a BOOL)
Had I not wanted to have the conditional "play-to-selection" capability, I could have called ::PlayEnhMetaFile in the OnDraw handler.
The Tree View (which holds the selection) must be queried to provide the selection. The nice thing is that using the broadcast mechanism of UpdateAllViews, I don't really have to know which view tells me that I can make a selection; all I care is that some view is going to tell me I can or cannot display the record.
I do make an assumption there, that the addresses of the records are monotonically increasing as we progress through the records. This is not an unreasonable assumption, and it is simpler to code using this. Since the records are nominally contiguous in memory, the implementation of metafiles seems to support this assumption.
To obtain information back, I cannot depend on a return value; instead, I pass in a pointer to a data structure and let the receiver use the information and set information in the data structure. The data structure used for a query is
class QueryShowInfo { public: QueryShowInfo(CONST ENHMETARECORD * r) {record = r; show = FALSE; } public: CONST ENHMETARECORD * record; BOOL show; };
The object holds the record pointer to the metarecord being considered, and has space for a BOOL to indicate if the metarecord should be executed. The show boolean is initialized to FALSE, so if nobody sets the show flag, the record will not be processed.
BOOL CMetafileExplorerDoc::QueryShow(CONST ENHMETARECORD * record) { if(mode == DoDrawAll) // [1] return TRUE; // [2] if(mode == DoDrawNone) // [3] return FALSE; // [4] QueryShowInfo query(record); // [5] UpdateAllViews(NULL, QRY_SHOWREQUEST, (CObject *)&query); // [6] return query.show; // [7] } // CMetafileExplorerDoc::QueryShow
The Draw All sets the document state to DoDrawAll mode
If we are in Draw All mode, don't even ask; just return TRUE
The mode is initialized to DoDrawNone
If no one has updated the mode to reflect a valid value, assume the result is FALSE
Declare a QueryShowInfo structure on the stack
Send a notification to all views, hoping that one will fill it in (actually, one will, the tree view, but this sender doesn't have to care which view actually responds)
Return the QueryShowInfo.show value set by the recipient view, whichever one cares
Note that I don't have to care about which view responds; I toss a request out over the fence, and somebody gets it and responds. I don't have to care who responds, and furthermore, even if nobody responds, I've set a value I can accept.
I have already shown the OnUpdate handler for one view; all views have the same handler. So for the CMetafileExplorerView (this is the tree view), I add to its Message Map
ON_REGISTERED_MESSAGE(QRY_SHOWREQUEST, OnQueryShowRequest)
The handler is a bit complex because the selection might be on any node of the tree; I want to draw up to and including the record that contains a selection. Thus, it was necessary to make sure that the selection refer to a metarecord.
Before explaining why this is required, we need to look at the EMF_RECORD handler:
ON_REGISTERED_MESSAGE(EMF_RECORD, OnAddRecord) // [1] LRESULT CMetafileExplorerView::OnAddRecord(WPARAM wParam, LPARAM) { CONST ENHMETARECORD * rec = (CONST ENHMETARECORD *)wParam; // [2] for(int i = 0; EMRdecode[i].name != NULL; i++) // [3] { /* decode it */ if(rec->iType == EMRdecode[i].id) // [4] { /* found it */ CTreeCtrl & ctl = GetTreeCtrl(); // [5] HTREEITEM root = ctl.GetRootItem(); // [6] HTREEITEM item = ctl.InsertItem(EMRdecode[i].name, root); // [7] ctl.SetItemData(item, (LPARAM)rec); // [8] DecodeRecord(item, rec); // [9] } /* found it */ } /* decode it */ return 0; } // CMetafileExplorer::OnAddRecord
The Message Map entry routes the message (the SendMessage from the OnUpdate handler) to the correct function
Cast the WPARAM to a generic ENHMETARECORD structure
Use the local decode-table to locate a printable string for the metarecord
Test the ENHMETARECORD.iType field against the table entry
Obtain the CTreeCtrl for this CTreeView
Obtain the root item; all metarecords are immediate descendants of the root item, which is the filename
Add the name of the metarecord to the end of the list of items underneath the root
Set the ItemData of the item added to be a pointer to the metarecord
Now decode the fields under the item.
The decode table uses another instance of my macro trick to create the table. Essentially, I did a copy of the EMR_ record definitions from wingdi.h, pasted it in, and did a quick replacement of the contents
#define EMRNAME(x) { _T(#x), x} static const struct { LPCTSTR name; UINT id; } EMRdecode[] = { EMRNAME(EMR_HEADER), EMRNAME(EMR_POLYBEZIER), EMRNAME(EMR_POLYGON), ... EMRNAME(EMR_RESERVED_119), EMRNAME(EMR_RESERVED_120), EMRNAME(EMR_COLORMATCHTOTARGETW), EMRNAME(EMR_CREATECOLORSPACEW), { NULL, 0}// EOT };
Now, given that I am assuming the records are presented in monotonic order of address which is based on the metafile layout, I can implement OnQueryShowRequest. Note: it is important to recognize here that the metafile handle must remain valid throughout the entire execution because the tree items refer to actual addresses in the metafile contents. Thus, if you are tempted to expand this to support some kind of editing, be aware that the metarecord pointers would have to be recomputed on a change!
LRESULT CMetafileExplorerView::OnQueryShowRequest(WPARAM wParam, LPARAM) { QueryShowInfo * info = (QueryShowInfo *)wParam; // [1] CTreeCtrl & ctl = GetTreeCtrl(); // [2] //================================================================ // +--filename <== A // | // +--- r1 <== B // | // +--- r2 <== C [selection example 1] // | | // | +--- field1 // | | // | +--- field2 <== C.2 [selection example 2] // | // +--- r3 <== D // | // : // | // +--- rn <== E //================================================================ HTREEITEM root = ctl.GetRootItem(); // [3] HTREEITEM sel = ctl.GetSelectedItem(); // [4] if(sel == NULL) // [5] { /* No selection */ info->show = FALSE; // [6] return 0; // [7] } /* No selection */ if( root == sel) // [8] { /* A: root selected */ info->show = FALSE; // [9] return 0; // [10] } /* A: root selected */ sel = GetMetaRecord(sel); // move up to record-level node // [11] // This is based on the fact that if there is an item selected, we would // encounter it. Note that we want to stop up-to-and-INCLUDING // the selected element HTREEITEM next = ctl.GetNextSiblingItem(sel); // [12] if(next == NULL) // [13] { /* E: at end */ info->show = TRUE; // [14] return 0; // [15] } /* E: at end */ ENHMETARECORD * nextrec = (ENHMETARECORD *)ctl.GetItemData(next); // [16] if(info->record >= nextrec) // [17] { /* D: after selection */ info->show = FALSE; // [18] return 0; // [19] } /* D: after selection */ info->show = TRUE; // [20] return 0; } // CMetafileExplorerView::OnQueryShowRequest
Convert the WPARAM to a QueryShowInfo pointer
Obtain the reference to the CTreeCtrl in the CTreeView
Obtain the root item
Obtain the selection item
If there is no selection, we are done
For no selection, there is nothing to show
All done: no selection was made
If the selection is the root item, we are done
For root item selected, there is nothing to show
All done: root selected
Make sure we are at an actual metarecord, not at some field definition within a metarecord. Note that we do not change the selection itself; we merely move our sel variable to represent the actual metarecord node that contains the selection
Obtain a handle to the next metarecord
If there is no next record, we are at the EMR_EOF, and we can certainly display up-to-and-including that
Indicate display is valid
All done: last item selected
Obtain the pointer to the metarecord of the next item following the selection
If the record we are asking about is greater than or equal to the record following the selection, it should not be shown
Indicate that the record is beyond the selection
All done: we're beyond the selection
Otherwise, we are at or below the selection; mark the display as being valid
To move from an arbitrary selection to the metarecord that contains the selection, we use the GetMetaRecord method. The test is based on the fact that a valid metarecord will have the root node as its parent.
HTREEITEM CMetafileExplorerView::GetMetaRecord(HTREEITEM item) { CTreeCtrl & ctl = GetTreeCtrl(); HTREEITEM root = ctl.GetRootItem(); if(item == root) return NULL; // not possible to get metarecord of root while(TRUE) { /* scan up in tree */ HTREEITEM parent = ctl.GetParentItem(item); if(parent == root) return item; // it is a valid metarecord item item = parent; } /* scan up in tree */ } // CMetafileExplorerView::GetMetaRecord
It should come as no surprise that the documentation for enhanced metarecords is, shall we say, of marginal quality. It is not clear how some of this documentation was written, but it is at best vague and in some cases so misleading or confusing that it would be hard for someone other than a highly-experienced programmer to make any sense of it, let alone use it. The documentation is incomplete and misleading. For example, the EMRSELECTOBJECT field occasionally, in real metafiles, contains the handle value 0x80000000, although this fact (and its meaning) is not documented anywhere. The documentation of EMRPOLYPOLYLINE, EMRPOLYPOLYGON, and EMR_POLYDRAW is so erroneous, confusing, and misleading that it is hard to guess what might have been intended, although I made my best guess, which I later confirmed by reverse-engineering the actual .emf file that was written. For a complete analysis, see my discussion in my essay on errors in Microsoft documentation. The following issues are discussed
I did not try to do every single field of every metarecord; at some point, I decided to "cut it off" and stop. For example, when an inline bitmap is involved, I should really pop up a window and display it, but probably only if the user double-clicks it. I might, in some obsessive moment, go back and add this feature. I have not verified my rendering of every EMR record, and some appear to be illegal.
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.