Envision, Create, Share

Welcome to HBGames, a leading amateur game development forum and Discord server. All are welcome, and amongst our ranks you will find experts in their field from all aspects of video game design and development.

Custom Control - Image List View Control

Hello,

I figured I'd create a topic dedicated to this, since it's a project all on its own.

I apologize to Tdata in advance for delaying the character database, but I think in the end it's worth it, overall.

The biggest worry I have with the entire project is ensuring that it's fast.  So the first thing I did was set up a series of regions dedicated to each drawing area: Columns, List Items and the non-client space in between.

So far I have little visually, the data's mostly there, but I don't have hardly any of the control logic laid out.  Though I figured this would be a good example to show others how I go about making controls.  Since there was some interest in other members on how to create visually interactive controls.

First off, there's a few things you need to keep in mind about a List View Control:
  • The Vertical ScrollBar Affects only the ListItem client area.
  • The Horizontal ScrollBar's maximum value is dependent upon the sum of the widths of the ColumnHeaders.
  • Cached Data is important - In order to expedite drawing, it's important to know a few things about the display order of the Groups, Columns and then the items within the Groups (if applicable).
    • I'll be doing experiments to determine the best case drawing habits.  I might cache the visible items on screen, should be small lists of 10-70 items at most.  In series of 60,000 items (likely scenario in file system displays), those lists will be very important in making drawing as fast as possible.
  • When drawing in .NET, make use of the [System]Brushes, [System]Fonts, [System]Pens, [System]Colors
  • If you use Regions, Fonts, Pens, Brushes, or anything else, that isn't part of the CLR's defaults, don't forget to dispose them when you're done with them, even if you cache them.
    • Your entire system is allotted 2^16 (16,384) GDI Objects, each Pen, Region, Font, Brush or whatever takes a single object handle.  If you exceed this maximum, your system becomes unstable.

For now that's all I can think of, I'll revise this list as I continue.  I'd be more scientific about it, but since I haven't had to make a control, like this, from scratch for a few years, it'll take some guess work, debugging and review, et cetera.

http://alexandermorou.com/images/screen ... review.png[/img]
It isn't much, but every journey begins with that first step.

For those interested, here's what I'm going to use to segment the drawing process:
(First three lines from OnPaint)
MergeDrawingAreasAndPaint(nonClientRegion, e.Graphics.Clip, e.ClipRectangle, e.Graphics,
    this.OnDrawNonClientArea);
MergeDrawingAreasAndPaint(columnHeaderRegion, e.Graphics.Clip, e.ClipRectangle, e.Graphics,
    this.OnDrawColumnHeaders);
MergeDrawingAreasAndPaint(listItemRegion, e.Graphics.Clip, e.ClipRectangle, e.Graphics,
    this.OnDrawListItems);
private static void MergeDrawingAreasAndPaint(Region finalRegion, Region originalClippingRegion,
    Rectangle originalClippingBounds, Graphics targetGraphics,
    PreEventHandler<PaintEventArgs> furtheredDrawing)
{
    if (finalRegion == null)
        throw new ArgumentNullException("finalRegion");
    if (originalClippingRegion == null)
        throw new ArgumentNullException("originalClippingRegion");
    if (targetGraphics == null)
        throw new ArgumentNullException("targetGraphics");
    if (furtheredDrawing == null)
        throw new ArgumentNullException("furtheredDrawing");

    //Make the region with the bounding box of the original.
    Region outputRegion = new Region(originalClippingBounds);

    //Intersect in the intended drawing area.
    outputRegion.Intersect(finalRegion);

    //Include complexity of original clipping region.
    outputRegion.Intersect(originalClippingRegion);
    if (!outputRegion.IsEmpty(targetGraphics))
    {
        targetGraphics.Clip = outputRegion;
        furtheredDrawing(new PaintEventArgs(targetGraphics,
            ConvertFromF(outputRegion.GetBounds(targetGraphics))));
    }
    targetGraphics.Clip = originalClippingRegion;
    outputRegion.Dispose();
}

Edit (December 6th, 2008 - 2:24 p.m.):
After looking closer at the purpose of the ScrollableControl, the original base of the ImageListView, I've decided to use Control as the base, and implement the ScrollBar handling myself via interception of Windows Messages.  The reason for this is the lack of the ScrollableControl to scroll without controls, in a way that's easy to manipulate (it kept screwing up the drawing mechanisms).

Edit 2 (December 6th, 2008 - 11:18 p.m.):
After reading up on the proper API calls, I decided to reference the Essential COM (Component Object Model) Interoperability project I'm writing.  While it's larger than the entire Control library and character database, it'll provide necessary information for handling COM errors and it's a good time to expand upon the library for Scroll Bar management.  The reason for the size is part of the feedback is a result of COM constructs mined from MSDN. Each message is contained within the HResult struct (a simple Int32-sized struct).  Within this is the Documentation comments, a property for each element (with a backing field), the HResult struct contains two enormous switch statements to pull the Description, and Name of the HResult.  I'll probably be replacing the Result Description switch statement with a call to FormatMessage, though this depends on how many messages are supported, since they vary system to system based upon drivers, if I read the documentation properly.

Size note: The ComErrors contains 52,795 lines of generated code, the HResult contains 12,022 lines.  There's also various other dll imports, COM interfaces, and general structures used to interact with the most frequently used COM.
 
I've finished coding the logic to manage the scrollbars.

The next step is to handle Column DisplayIndex vs. Physical index, display reordering, and the basic data management logic, without writing a single line of the UI to do the same.

This way the UI has something to base its data management on, it'll improve the overall consistency of how things are done.

I'll do similar ordering data management with the list items themselves.  The X/Y locations of the sub-items won't even be touched upon since they're directly dependent upon the ListItem order (Y) and the Column order (X).  I'll edit this post with more information once it's available.
Edit (December 8th, 2008 - 1:07 PM):
On the ScrollBars, so that the calculations are kept at a minimum: instead of recalculating the location of the list items every time the user changes the scrollbar location, on top of clipping the client area, a simple transformation is applied to the graphics object we'll be using to draw everything.
For the Column Headers, included with the horizontal scroll bar's value, the x/y top of the bounding area for the column headers:
e.Graphics.TranslateTransform(-hsVal + columnHeaderArea.X, columnHeaderArea.Y);

Right now I'm getting ready to include code to make the scrollbar's update the active area of the control.  There's not much yet, visually, so I won't be posting a screenshot as of yet.

Edit (3:08 PM):
Finished up the scrollbar movement code, here's a basic idea of how the column headers will look.  I don't have images on them quite yet.
http://alexandermorou.com/images/screen ... iew-02.png[/img]
 
This -> @"\1\[\b\!\b\]\0\You have received \i2\Red Fish\i\"
Is Parsed by This -> @"(\\([^\\]+)\\)?([^\\]+)?"
resulting in This ->http://img516.imageshack.us/img516/637/mootyf0.png[/img]

Code:
        private void ParseInputMessage()
        {
            _outputMessage = "";
            bool underline = false, bold = false, italics = false;
            Color fontColor = Color.Black;
            Point offset = new Point(0,0);

            _graphics.Clear(_backgroundColor);
            string pattern = @"(\\([^\\]+)\\)?([^\\]+)?";
            MatchCollection matches = Regex.Matches(_inputMessage, pattern, RegexOptions.Multiline);
            foreach (Match match in matches)
            {
                if (match.Success)
                {
                    string arguments = match.Groups[2].Value;
                    string message = match.Groups[3].Value;

                    /* Parse Each Argument */
                    for (int i = 0; i < arguments.Length; ++i)
                    {
                        switch (arguments[i])
                        {
                            case 'u':
                                underline = !underline;
                                break;
                            case 'b':
                                bold = !bold;
                                break;
                            case 'i':
                                italics = !italics;
                                break;
                            case '0':
                                fontColor = Color.Black;
                                break;
                            case '1':
                                fontColor = Color.Red;
                                break;
                            case '2':
                                fontColor = Color.Green;
                                break;
                            case '3':
                                fontColor = Color.Blue;
                                break;
                            case '4':
                                fontColor = Color.Orange;
                                break;
                            case '5':
                                fontColor = Color.Yellow;
                                break;
                            case '6':
                                fontColor = Color.Aqua;
                                break;
                            case '7':
                                fontColor = Color.White;
                                break;
                            case '8':
                                fontColor = Color.Crimson;
                                break;
                            case '9':
                                fontColor = Color.Lime;
                                break;
                        }
                    }

                    /* Set Font Attributes */
                    FontStyle style = FontStyle.Regular;
                    if (bold) style |= FontStyle.Bold;
                    if (underline) style |= FontStyle.Underline;
                    if (italics) style |= FontStyle.Italic;
                    _font = new Font(_font, style);

                    /* Render Font */
                    using (SolidBrush brush = new SolidBrush(fontColor))
                        _graphics.DrawString(message, _font, brush, offset);

                    /* Update offset */
                    SizeF msgsize = _graphics.MeasureString(message, _font);
                    offset.X += (int)msgsize.Width;
                    _outputMessage += message;
                }
            }
        }

Add some flavor. :3
 
I'm not adding that, below explains why.
First: The control is a List, per-item coloring to that degree would be costly on sets of data around 65,000 rows and 6 columns (after all that's 390,000 strings just on that).  While I won't be redrawing all of them all the time, that would be caching 390K regular expression matches. 
Second: I don't see you disposing your font.  Scrolling a window of ~65 thousand entries, each using potentially multiple fonts, would very quickly overrun your system's GDI objects.  Unless the documentation on MSDN is incorrect and specifying the font in the constructor makes a new font, and automatically disposes the old, but I don't think it does.  Unless of course, your _font local is a property that disposes the old font when a new one is set.
Third: The columns will have a ForeColor property, should that satisfy the flavor you're wanting?  I think too many variations in color on a dialog tend to detract from the consistency and might confuse users as to why they're so colorful.  Do the colors have meaning?  Why is this red and that blue?  For standard use, there's no need to highlight things in that manner.

I'm not trying to be mean here, I'm just saying that for the end goal, colorizing the text to that degree isn't necessary.  The list isn't done, hopefully it'll satisfy most, visually, once it is.
 

Injury

Awesome Bro

Way to be unappreciative! It's obvious that he is working on something that means he can take more time on his other project! Be a bit more supportive and maybe reply with something helpful/constructive?

Alexander, is this written in c# or C++? or something else entirely?
 
I'm using C♯.

I've got the control doing the hit-testing for the columns, right now the next step is calculating the vertical space for the items, which will only be done when items are added or when the control's resume layout method is called (when it's suspended, it'll track what changes were made using a series of flags, those flags on resume tell what to calculate next, this is for mass insertions).

It'll only really calculate the item metrics (size) when they change (text/type), so the vertical space pass should be pretty quick even on adding items, since it caches the old values.  The location on the other hand, can be pretty dynamic, due to sorting, and so on.

To answer wimomacon's question:  I'm presently taking a break due to building a portfolio.  Getting finished projects under my belt is just as important as the overall project.  The controls I make here will be useful for the IDE aspect of the game creation software.  It might take time, and you could say 'Well he's not working on it'.  I am, I'm just very picky about User Interfaces.  They're as important as the engine itself since: if you can't use the program because it's visually unintuitive, what good is it?

Edit: Also, I'm planning on the entire background of the control being graduated, is this something needed or useless?

Presently it scrolls the graduated area fairly OK, there's a bit of tear, but nothing too terrible.  There's also slight flicker on resize, but only when the width of the columns is less than the control's width (the graduated background is distributed over the longest width, the control width or the right side of the last column).  Otherwise it seems fairly flicker-free.  I want to stay away from double buffering for cases where people are on remote desktop sessions (not likely a case, but you never know).  Double buffering the control would greatly slow their system when doing anything with the program, due to the nature of a buffer.  I'm not sure how calls to ScrollWindow are handled over remote desktop, anyone here know?
 
Alexander, if you've got the Drawings being done in WMPaint which is determined via SetStyle(); then it shouldn't flicker at all;
If it is then maybe you need to set the OptimizedDoubleBuffer. :S

Unfortunatly I don't do much .Net Control stuff; So I cannot help w/ certain things.
Though I've never had the flicker problem w/ one or both of those set.
 
I'm going to put this on hold for now.

I'd prefer to finish the Character Database sometime soon.  I ran into a few issues with the custom ListView control, namely speed issues (on sets of data around 32000 items).  I think a large part of it has to do with it being written entirely in a managed language.  Every action therefore has an impact.  The regular ListView handles everything in a non-managed way that has a managed wrapper around the parts they expose.  The HitTesting, sorting, item bounds checks, redraw code, and so on are trivial by themselves, but when you layer them on top of each other, things start to get complex, pretty quickly.  For the most part the control works, but it's not super fast, like I want it to be.  I'll probably resume this later, when I can devote time to it.
 

Thank you for viewing

HBGames is a leading amateur video game development forum and Discord server open to all ability levels. Feel free to have a nosey around!

Discord

Join our growing and active Discord server to discuss all aspects of game making in a relaxed environment. Join Us

Content

  • Our Games
  • Games in Development
  • Emoji by Twemoji.
    Top