A game engine is only as useful as the tools used to create games for it. Following this line of logic, it seems obvious that I also need to create a game studio to go along with the game engine. Aside from creating sprites, animations, and the like, a good game studio is going to have a good syntax-highlighting text editor for writing scripts. Now, I don't know how many people have tried to created a syntax-highlighting text box before, but I can say that I have, and that it is not as easy as it sounds. I don't just want syntax highlighting, but also automatic pretty-code formatting. If you have ever used Microsoft Visual Studio, you already have an idea of what I'm talking about.
The obvious solution would seem to be a RichTextBox. We could create an even handler that consumes the OnTextChanged event, and parse the the text in the box. We would look for tokens and change their color in the RichTextBox, for example, green for comments, blue for keywords, red for text, and so on. I did just this, using regular expressions to locate keywords, comments, and double quote delimited text, and colorize them. Unfortunately, this doesn't help with code formatting, only coloring. It also is SLOW! Even only handling highlighting on a single line basis, typing even moderately fast would cause the RichTextBox to stop updating text as the regex routine ground down to a near halt. Handling pretty code formatting is an equal nightmare, as indenting and text replacing kept moving around the system caret (the cursor where you type), and screwing up text entry when typing fast.
So, I've set off on creating my own custom UI control. I figure since I'm making the whole thing from scratch anyway, I might as well have some fun with it. I've decided to make the CodeControl a text-box like region with scroll bars (horizontal and vertical), no word wrapping (terrible for coding anyway), and a combo box at the top. The combo box would populate a list of declared functions that the user can choose from to auto-scroll to the definition of that function. I thought it would be a nice touch.
To pull this off, we need to consume KeyPress, MouseClick, and OnPaint events. We need to override any default rendering so we can paint it how we want, including formatted and colored text. We need to implement our own caret (text cursor), so we can account for it when formatting code. We also need to handle text selecting, drag-drop moving, copy, paste, cut, undo, and redo for maximum user friendliness. Oh, and the control has to run fast and efficient.
To prevent reinventing the UI wheel, it would be nice if we could inherit our new control from an existing UI control. Text box is an obvious control to inherit from, but TextBox has a system generated caret that we can't (easily) hide and show, except giving and taking focus which messes with our KeyPress handler. So TextBox is out.
PictureBox is designed to be efficient to draw to using GDI+, and includes many optimizations by default to increase performance. PictureBox has a major limitation though: no KeyPress event. There is a KeyDown and KeyUp event, but they don't ever fire because PictureBox is incapable of accepting focus. So PictureBox is out.
RichTextBox has the same issue as TextBox, so it is out.
Panel can't accept focus either, so it has the same problem as PictureBox.
GroupBox doesn't accept focus normally, but if we call the Focus method from MouseClick event, and watch our GotFocus and LostFocus events, we can appropriately turn on and off our custom caret. GroupBox doesn't normally accept focus, but for some reason it does have the KeyPress event (I suspect it is inherited from something, and is only there to pass input to child objects.), although the MSDN says that KeyPress never fires, I called Focus, and pressed some keys, and sure enough KeyPress was firing, so that's good.
So it looks like GroupBox has all the necessary events and functionality we need to implement a new text box, but a group box doesn't really look much like a text box. That's easy enough to change. We can use the paint event to Graphics.FillRectangle the ClientRectangle to white, then draw a dark gray rectangle around the ClientRectangle area. That should do it for looks. Only trouble is GroupBox is not a control optimized for user drawing. Anything we paint is going to cause flickering on the control when we refresh, which is going to happen every keystroke, and every 1/2 second for the caret blink. Ugh.
I added a project "SyntaxTextBox" to the "Game Studio" solution, set the build order so that SyntaxTextBox was a dependancy of the project "Main Editor". Then I added a reference of SyntaxTextBox to the Main Editor project. I added a user control "CodeControl" and a class "EnhancedTextBox" to the SyntaxTextBox project. I right clicked SyntaxTextBox and click Build. Then Save All
Now, on the Main Editor project, I can add an instance of the CodeControl to the main form. This allows me to debug my custom user control as it edit it. I just have to make sure I always right click the UI project and click build before my changes will show up under the main form.
On CodeControl, I added a combo box, a HScroll and VScroll. I opened the code view on CodeControl, and created a public instance WithEvents for the EnhancedTextBox. In Public Sub New of CodeControl, I added the control to Me.Controls (after InitializeComponent), set the location, size, and anchor properties.
In EnhancedTextBox, I inherit Groupbox in Public Class EnhancedTextBox, which essentially turns my EnhancedTextBox into a groupbox (for now).
I built the project, and added the control to the main form and build the project. As I expected, the custom control now has a combo box at the top, scroll bars on the right and bottom, and a group box filling the rest of the control.
Now on to turning the group box into a text box. I opened the code in EnhancedTextBox, and added the methods Public Sub New, Protected Overrides Sub OnPaint, and the Public Event DrawMe.
Here's the EnhancedTextBox class now:
Public Class EnhancedTextBox
Inherits GroupBox
Public Sub New()
MyBase.New()
Me.SetStyle(ControlStyles.AllPaintingInWmPaint Or ControlStyles.UserPaint Or ControlStyles.OptimizedDoubleBuffer, True)
Me.UpdateStyles()
End Sub
Public Event DrawMe(ByRef e As System.Windows.Forms.PaintEventArgs)
Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
'don't need this, we are doing user painting
'MyBase.OnPaint(e)
'clear client area
e.Graphics.FillRectangle(Brushes.White, Me.ClientRectangle)
'draw boarder
e.Graphics.DrawRectangle(Pens.DarkGray, Me.ClientRectangle)
'call the draw event where text painting will occur
RaiseEvent DrawMe(e)
End Sub
End Class
SetStyle allows me to set flags for the control that determines how it will behave.
AllPaintingInWmPaint effectively disables the default group box painting code
UserPaint tells the blitter that I'm going to be painting, so don't update the screen until I'm done.
OptimizedDoubleBuffer tells the blitter that I want to draw to an off screen plane, then present the whole control at once instead of drawing each item to the screen one at a time. This gets rid of flickering.
Now in the CodeControl, we have a group box that isn't a group box any more, but will still behave as if it were one. We need to change that.
We need to write a handler for MouseClick. This will manually set focus on the group box so that KeyPress will function. This method will also need to store the mouse_x, and mouse_y into a public variable, and flag that a click occurred in the group box.
We need to write a handler for GotFocus that turns on a timer control (500 msec interval), and flags the caret on. GotFocus will also trigger a refresh for the groupbox.
We need to write a handler for LostFocus that turns off the timer, and flags the caret off.
We need to write a handler for the timer_tick that toggles the carret on/off.
We need to write a handler for KeyPress that handles keystrokes (updating text manually, checking for keywords, setting color, etc). I will use a select case block to determine what keys were pressed, and check for current states, such as "is text selected? This key code replaces that text", and "are we in insert or overwrite mode?". Some reference codes: Asc(e.KeyChar): 8 = Backspace key, 9 = Tab Key, 12 = Enter Key. 27 = Escape.
We need to write a handler for KeyDown/KeyUp that handles Shift, Alt, Control, Arrow keys, F1 .. F12, Delete key, Page Up, Page Down, Insert (for insert/overwrite mode), and alt-menu key.
We need to write a handler for the custom DrawMe event that actually draws the text on the screen. If the click occurred flag is set, we need to calculate the caret line and column location while drawing the text on the screen. This is not so obvious to do since some fonts are proportional. We'll have to check each character as we draw it to see if the mouse clicked over it's location on screen.
We need to write a handler for MouseDown/MouseMove/MouseUp that tracks mouse movement while buttons are down to handle text selection. I haven't worked out exactly how this will work, but I have a general idea that it will be tied with the set caret code from MouseClick.
Some of these handlers I'm working on now, and are in a basic skeleton format. The result is I have a marginally functional text box that I can begin to do whatever I want with it.
There is a lot of code still to be written before this custom text box is ready for prime-time though. Sounds like some straightforward programming fun to me!
Hopefully, this will give someone a basic idea for how to implement their own custom UI elements.
No comments:
Post a Comment