Irecently was asked a question by a reader who is developing a graphical editor that supports non-Windows® objects (rectangle, line, circle, image) as well as a Windows object (a text control derived from the rich text control). He wanted to define the Z-order of these objects, but discovered that the text window always hides other objects.
Figuring out how to implement the Z-order took some time and research. I've seen discussions about making controls transparent, but I couldn't find a good answer, so I tried a few approaches of my own to solve the problem. To test solutions, I decided to focus on the problem of hosting graphics such as lines, boxes, circles and such. Let's walk through a few approaches and see what happens.
Using a Windows Form to host the objects allows me to put the items on the form and then make the form transparent, thus rendering the items beneath it except for those objects that are on the form itself. I tried this by creating two forms and making one of the forms transparent (by setting the TransparencyKey to the value of the form's BackColor). This seemed to work fine. Then I put this form on the master form as a subform.
To create a subform on a Windows Form you must set the TopLevel property of the subform to False and then create an instance of the subform in the parent, and add the subform to the controls collection of the parent. This will place the subform on the parent form. This approach is pretty slick and it even works in design view in Visual Studio® .NET so you can see your form design while building the application. You might notice that I have not shown you any code yet. I will get to that in a minute when I start describing what works best.
The problem with using a subform approach is that the transparent form functionality only works if the form has its TopLevel property set to True. As I mentioned, when you use a form as a subform you must set the TopLevel property to False and thus you cannot use transparency with that form. I was quite disappointed as this would have been an elegant solution that would allow you to do really creative things with your applications.
The second approach also uses a Windows Form as a host for child objects. I had more success with this one than with the subform. This time I set up the second form with a transparent background but the problem is that you have to keep these forms in sync with the main form. For instance, if the user minimizes the main form, you must minimize all of the palette forms. If the main form moves, you must move the palette forms. This requires lots of code and much fiddling to get it right. If you must host items such as non-Windows objects that you can render with GDI+, then this form approach might be appropriate. It can be done, but it will take a ton of work to keep the many forms synchronized. Although the transparent form requires a lot of effort, it does work. In the process of debugging this approach, however, I hit on a solution that seems to work even better.
This approach does everything in one form except for a drawing palette which is implemented by a second form. It works great but it still needs some fine tuning, which I'll leave as an exercise for you to do on your own.
First, I created a simple form that has two controls. The first control is a button called Drawing Palette. Then I added a RichTextBox and a MainMenu (File Open and Close). So far, this approach is really simple. Now for the fun.
I wanted the application to allow me to open a rich text file (.rtf), then add graphics to it. I could have just used GDI+, but that would not allow the user to add graphics at run time. So, I decided to add a drawing palette as another form, place it at a specific location over the main form, and allow users to draw on it. After much playing around with this extra form approach, I finally found a solution for handling graphics, which I will show you now.
As you can see, Figure 1 shows a simple text file opened in the application with simple graphics displayed over the text. The text is shown in the RichTextBox.
Figure 2 shows the drawing palette on the Grapher form overlaying the textbox. Grapher is a simple form with one key setting. Its Opacity property is set to 50 percent. This allows the underlying text in the RichTextBox to show through this form. Grapher also has all the widgets (such as border, control box, and so on) turned off, resulting in a borderless form. The toolbar is actually a panel control housing three pictureboxes with the graphics for the buttons.
The version I wrote only supports drawing a straight line and a rectangle. The user can see the underlying text beneath the palette. This allows the user to position the graphics over the text visually. Once finished, the user can click the X to close the palette or display the palette later to add more graphics.
Figure 3 shows the key code from Form1. I have omitted most of the generic code in Figure 3 and Figure 4, such as variable definitions and opening files, to focus on the relevant code. The DrawingPaletteButton_Click event is used to start a drawing operation by allowing the user to drag out the size and location of the drawing palette over the RichTextBox. The DrawingPaletteButton_Click code sets up the cross-hair cursor and sets the DrawingMode variable to True.
The MainTextRichTextBox_MouseDown sets up the drag operation when the user clicks the mouse button. The MainTextRichTextBox_MouseMove event actually draws the rectangle. When the user releases the mouse button, MainTextRichTextBox_MouseMove fires and the palette is drawn by displaying the Grapher form, as you saw in Figure 2.
Now, let's move to the code in Figure 4. When implemented, the user can click either the line or rectangle graphic shown on the toolbar in Figure 2 and then draw that object with the mouse. Grapher also uses the DrawingMode enumeration to track which type of graphic to draw (line or rectangle). The user can draw as many items on a single palette as they want. Then the user clicks the X on the toolbar.
The drawing operations in Grapher are similar to those in Form1. However, instead of throwing away the rectangle, the definition for the drawing operation is stored in the DrawingItem structure. Then when the user releases the mouse, the instance of the structure is stored in the ThisDrawingItems collection. Finally, when the user clicks the X, the palette is closed by calling CloseEditModePicturebox_Click. This event uses the DrawingItems collection in Grapher to update the DrawingItems collection in Form1.
Refer back to the Paint event in Figure 3. This event calls the PaintGraphics procedure to actually perform the work. The Paint event in Grapher is very similar, except that it does not call a subroutine to handle it. The code in both events processes the DrawingItems (Form1) and ThisDrawingItems (Grapher) and uses GDI+ to display the graphics on the RichTextBox.
The collection of drawing items allows you to easily move drawing items from the palette form to the main form. Thus, you do not need to worry about trying to have a transparent form or control to host the graphics. Instead, the code just draws the graphics on the main form's RichTextBox.
Let's consider drawing everything using GDI+. You could treat each block of text and each graphic as an object and store them in a collection. I would create a custom structure and enum like this:
Enum ObjectTypes
Text = 0
Bitmap = 1
EMF = 2
End Enum
Structure EditorObject
Dim ThisObject
Dim ThisObjectType As ObjectTypes
Dim X As Integer
Dim Y As Integer
Dim Z As Integer
End Structure
Now, I could create an object and put it in a collection. Next I would iterate through the collection of items and draw them. Since each object has a Z value, I could use that to determine the object's placement relative to the other objects.
As you can see, sometimes the solution to a problem is rather simple. Since just about everything is an object in .NET, you can often get around limitations by using a collection to hold the object and manipulate it. Since I am using a collection of graphics items, I could even save the items to disk alongside the text file and reload them later. I like the third approach I presented as it allows me to separate text and graphics and it makes it easy to use the RichTextBox as the text engine. This way I don't have to devise a cool way to handle the text; I just draw the graphics over the text. The Microsoft® .NET Framework does not limit you in this respect; it usually lets you do it either way.
One last note: the GDI+ code shown in this little app needs some tweaking before it is put into production. I did not fine-tune the coordinate mapping or provide other features that you could easily add to meet your needs.
Send your questions and comments for Ken to basics@microsoft.com.