Advanced Basics
Create a Graphical Editor Using RichTextBox and GDI+
Ken Spencer
Code download available at:
AdvancedBasics0405.exe
(130 KB)
Browse the Code Online

Contents
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.
Approach 1: The Subform
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.
Approach 2: The Transparent Form
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.
Approach 3: GDI+ and RichTextBox
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.
Figure 1 Simple Text File
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 Drawing Palette Over Text
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.

Figure 4 Key Code Elements from Grapher.vb
|
Friend Structure DrawingItem
Dim DrawingMode As DrawingModes
Dim StartPostion As Point
Dim EndPostion As Point
Dim BoxSize As Size
Dim PaletteX As Integer
Dim PaletteY As Integer
End Structure
Friend Enum DrawingModes
Line
Rectangle
None
End Enum
Private Sub CloseEditModePicturebox_Click( _
ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles CloseEditModePicturebox.Click
Dim ThisDrawItem As New DrawingItem
If IsNothing(ThisParentForm.DrawingItems) Then
ThisParentForm.DrawingItems = New Collection
End If
For Each ThisDrawItem In ThisDrawingItems
ThisParentForm.DrawingItems.Add(ThisDrawItem)
Next
If Not IsNothing(ThisParentForm.DrawingItems) Then
ThisParentForm.PaintGraphics()
End If
Me.Hide()
End Sub
End Class
|

Figure 3 Key Code Elements from Form1
|
Private Sub DrawingPaletteButton_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles DrawingPaletteButton.Click
Try
Me.MainTextRichTextbox.Cursor = Cursors.Cross
DrawingMode = True
Catch exc As Exception
End Try
End Sub
Private Sub MainTextRichTextbox_MouseDown(ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) _
Handles MainTextRichTextbox.MouseDown
DrawBoxX = e.X
DrawBoxY = e.Y
If DrawingMode Then
MouseDownDraw = True
End If
End Sub
Private Sub MainTextRichTextbox_MouseMove(ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) _
Handles MainTextRichTextbox.MouseMove
If Not MouseDownDraw Then
Exit Sub
End If
If e.X > DrawBoxX Then
BoxWidth = e.X - DrawBoxX
StartX = DrawBoxX
Else
BoxWidth = DrawBoxX - e.X
StartX = e.X
End If
If e.Y > DrawBoxY Then
BoxHeight = e.Y - DrawBoxY
StartY = DrawBoxY
Else
BoxHeight = DrawBoxY - e.Y
StartY = e.Y
End If
Me.Refresh()
MeGraphics.DrawRectangle(LinePen, _
StartX, StartY, BoxWidth, BoxHeight)
End Sub
Private Sub MainTextRichTextbox_MouseUp( ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs) _
Handles MainTextRichTextbox.MouseUp
If DrawingMode Then
Me.MainTextRichTextbox.Cursor = Cursors.Default
frmGrapher.StartPosition = FormStartPosition.Manual
frmGrapher.XFormOffset = StartX
frmGrapher.YFormOffset = StartY
GraphicxLoc = Me.Location.X + _
Me.MainTextRichTextbox.Location.X + StartX + XOffset
GraphicyLoc = Me.Location.Y + _
Me.MainTextRichTextbox.Location.Y + StartY + YOffset
Debug.WriteLine("MainTextRichTextbox_MouseUp:" & _
" graphicxloc= " & GraphicxLoc & _
" graphicyloc= " & GraphicyLoc)
frmGrapher.Location = New Point(GraphicxLoc, GraphicyLoc)
frmGrapher.Size = New Size(BoxWidth, BoxHeight)
frmGrapher.ResetDrawingItems()
frmGrapher.ShowDialog()
End If
DrawingMode = False
MouseDownDraw = False
End Sub
Private Sub MainTextRichTextbox_SelectionChanged(_
ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles MainTextRichTextbox.SelectionChanged
If DrawingMode Then
MainTextRichTextbox.Select(0, 0)
End If
End Sub
Private Sub Form1_Paint( ByVal sender As Object, _
ByVal e As System.Windows.Forms.PaintEventArgs) Handles MyBase.Paint
PaintGraphics()
End Sub
Friend Sub PaintGraphics()
Dim ThisDrawItem As New Grapher.DrawingItem
Dim xLoc, yLoc As Integer
Dim xEndLoc, yEndLoc As Integer
MainTextRichTextbox.Refresh()
If Not IsNothing(DrawingItems) Then
For Each ThisDrawItem In DrawingItems
With ThisDrawItem
.PaletteX = .PaletteX
.PaletteY = .PaletteY
xLoc = .PaletteX + .StartPostion.X
xEndLoc = (.PaletteX) + .EndPostion.X
yLoc = .PaletteY + .StartPostion.Y + (YOffset - 50)
yEndLoc = .PaletteY + .EndPostion.Y + (YOffset - 50)
If Not DebugOutputComplete Then
DebugOutputComplete = True
End If
Select Case ThisDrawItem.DrawingMode
Case Grapher.DrawingModes.Rectangle
MeGraphics.DrawRectangle(LinePen, _
xLoc, yLoc, .BoxSize.Width, .BoxSize.Height)
Case Grapher.DrawingModes.Line
MeGraphics.DrawLine(LinePen, xLoc, _
yLoc, xEndLoc, yEndLoc)
Case Else
End Select
End With
Next
End If
'Test guidelines
If ShowGuidesMenuItem.Checked Then
MeGraphics.DrawLine(LinePen, 60, 0, 60, Me.Height)
MeGraphics.DrawLine(LinePen, 0, 60, Me.Width, 60)
End If
End Sub
Private Sub MainTextRichTextbox_TextChanged( _
ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles MainTextRichTextbox.TextChanged
PaintGraphics()
End Sub
Private Sub Form1_Resize(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles MyBase.Resize
If Not MeGraphics Is Nothing Then
MeGraphics.Dispose()
End If
MeGraphics = Me.MainTextRichTextbox.CreateGraphics
End Sub
|
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.
One More Solution
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.
Conclusion
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.
Ken Spencer works for 32X Tech (
http://www.32X.com), where he provides training, software development, and consulting services on Microsoft technologies.