By itself, a ListView can draw a piece of text with an optional image (details view has more than one piece of text). For the majority of cases, this is more than enough. But once in a while, there's a situation where a picture is worth more than one thousand hours of development. Hopefully, with an ObjectListView, it will take much less time than that.
Like most of ObjectListView
, owner drawing is accomplished by installing a delegate. Inside the renderer delegate, you can draw whatever you like:
this.occupationColumn.RendererDelegate = delegate(EventArgs e, Graphics g, Rectangle r, Object rowObject) { using (LinearGradientBrush gradient = new LinearGradientBrush(r, Color.Gold, Color.Fuchsia, 0.0)) { g.FillRectangle(gradient, r); } StringFormat fmt = new StringFormat(StringFormatFlags.NoWrap); fmt.LineAlignment = StringAlignment.Center; fmt.Trimming = StringTrimming.EllipsisCharacter; fmt.Alignment = StringAlignment.Near; g.DrawString(((Person)rowObject).Occupation, listViewComplex.Font, Brushes.Black, r, fmt); return false; };
This delegate produces this spectacularly garish result:
Installing a delegate works fine, but there are numerous utility methods that would be useful within such a delegate:
The BaseRenderer
class encapsulates these utilities. You can use this class directly, use one of the existing subclasses (see below) or roll your own subclass to do whatever you want. To actually use one of these renderers, you assign an instance of them to a column's Renderer
property, like this:
colCookingSkill.Renderer = new MultiImageRenderer(Resource1.star16, 5, 0, 40);
There are a couple of flavours of BaseRenderer
already available for use.
This is a simple-minded horizontal bar. The row's data value is used to proportionally fill a "progress bar." The manner of drawing the progress bar is customisable. This example shows using the default system theme renderer.
A BarRenderer
can be initialised in a couple of different ways. The following code creates a renderer that will draw a progress bar in the range 0-2 (used for a person's height in metres) with a standard themed bar.
this.olvHeight.Renderer = new BarRenderer(0, 2);
And this code creates a renderer that will draw the same data values as above, but will do so with a hideous horizontal gradient, framed in black.
this.olvHeight.Renderer = new BarRenderer(0, 2, Pens.Black, Brushes.Gold, Brushes.Blue);
This renderer draws 0 or more of the same image based on the row's data value. The 5-star "My Rating" column on iTunes is an example of this type of renderer.
Initialising one of these renderers is a little more complicated. It needs to know that image that should be drawn, the minimum and maximum values to be considered, and the maximum number of images that the renderer should draw. If the value to be drawn is less than the minimum, no images will be drawn. If the value is greater than the maximum, then the maximum number of images will be drawn. For values between the minimum and maximum, a proportionally number of images will be drawn.
this.olvCookingSkill.Renderer = new MultiImageRenderer(Resource1.star16, 5, 0, 40);
This says that when drawing the cooking skill column, 0 or more copies of the star16 image will be drawn. At most, 5 images will be drawn. If the person's cooking skill is 0 or less, no images will be drawn. If their skill is 40 or greater, 5 stars will be drawn. Otherwise, 1 + skill / ((maximum - minimum) / maxNumberImages)
images will be drawn.
This renderer draws an image decided from the row's data value. Each data value has its own image. A simple example would be a Boolean
renderer that draws a tick for true
and a cross for false
. This renderer also works well for enum
s or domain-specific codes.
This type of renderer is initialised to map a particular value to the image that should drawn for the value.
this.olvRank.Renderer = new MappedImageRenderer(new Object[] { Rank.Private, Resource1.PrivateInsignia, Rank.Corporal, Resource1.CorporalInsignia, Rank.Sergeant, Resource1.SergeantInsignia, });
This renderer can also render a collection of values. If the Aspect
returns a collection, rather than just a simple value, then each item in that collection will be drawn side by side.
This renderer tries to interpret its row's data value as an image. Most typically, if you have stored Image
s in your database, you would use this renderer to draw the images from the database. It also works with paths that lead to local image files.
As the result of a burst of misguided enthusiasm, this renderer will also draw animated graphics. Of what possible use that would ever be in a real world application, I'm not sure, but it's comforting to know that the option is always there should you choose to use it. The code works on GIFs and may well work on other frame-based animation formats, but I have not had the opportunity (or inclination) to try.
This is initialised very simply:
this.olvImage.Renderer = new ImageRenderer();
This renderer draws zero or more images horizontally. The Aspect
for this renderer must return a collection of imageSelectors, where an imageSelector can be an integer, a string, or an Image
object. The integer and string will be used as indexes into the SmallImageList
, while the Image
will be rendered directly.
This too is initialised very simply:
this.olvTellsJokes.Renderer = new ImagesRenderer();
A FlagRenderer
is similar to an ImagesRenderer
in that it draws zero or more images horizontally. It differs in how it decides which images to show. A FlagRenderer
expects that it's Aspect
will be a collection of flags, that is, a bitwise OR'ed collection of exclusive values. When the FlagRenderer
is created, it is initialised to say, "When you see this bit set, draw this image".
So for this example, a FlagRenderer
was created to render FileAttributes
values. The renderer was then configured so that when it saw a FileAttributes.Archive
flag, it draw the "archive" image from the SmallImageList
. Similarly for the FileAttributes.ReadOnly
flag and the other flags.
FlagRenderer<FileAttributes> attributesRenderer = new FlagRenderer<FileAttributes>(); attributesRenderer.Add(FileAttributes.Archive, "archive"); attributesRenderer.Add(FileAttributes.ReadOnly, "readonly"); attributesRenderer.Add(FileAttributes.System, "system"); attributesRenderer.Add(FileAttributes.Hidden, "hidden"); this.olvColumnAttributes.Renderer = attributesRenderer;
The images are drawn in the order in which the flag/image pairs are added to the renderer. If FileAttributes.System
was added first, the "system" image would be drawn first.
These pre-existing flavours of BaseRenderer are handy, but they don't exhaust all possibilities.
Suppose that you have an abiding love of garish gradients, and you really want to be able to use them easily and repeatedly. Your first step would be to subclass BaseRenderer
and override the Render(Graphics g, Rectangle r)
method. Within that method, you do the drawing that you want:
public class GradientRenderer : BaseRenderer { public override void Render(Graphics g, Rectangle r) { using (LinearGradientBrush gradient = new LinearGradientBrush(r, Color.Gold, Color.Fuchsia, 0.0)) { g.FillRectangle(gradient, r); } StringFormat fmt = new StringFormat(StringFormatFlags.NoWrap); fmt.LineAlignment = StringAlignment.Center; fmt.Trimming = StringTrimming.EllipsisCharacter; switch (this.Column.TextAlign) { case HorizontalAlignment.Center: fmt.Alignment = StringAlignment.Center; break; case HorizontalAlignment.Left: fmt.Alignment = StringAlignment.Near; break; case HorizontalAlignment.Right: fmt.Alignment = StringAlignment.Far; break; } g.DrawString(this.GetText(), this.ListView.Font, Brushes.Black, r, fmt); } }
With this renderer, you can now render any column on any ObjectListView
with your colourful background. By using a StringFormat
, this renderer will automatically align the text according to the alignment of the column, truncate too long strings, and center the text vertically when necessary.
Obviously, if this was a real class, you would replace Color.Gold
, Color.Fuchsia
, and 0.0
with properties like StartColor
, EndColor
, and GradientAngle
respectively, but you get the idea.
If you are keen, you can also owner draw the other views too, the non-Details views.
To do this, you install a renderer on your primary column (column 0), and do your rendering like normal. The only slight difference is that your renderer will have to check which view the ObjectListView
is currently using before doing your rendering Suppose you only want to inflict your gradients on your users when the ObjectListView
was in Tile
view. You would change your above renderer to be something like this:
public class TileViewGradientRenderer : BaseRenderer { public override bool OptionalRender(Graphics g, Rectangle r) { if (this.ListView.View != View.Tile) return false; using (LinearGradientBrush gradient = new LinearGradientBrush(r, Color.Gold, Color.Fuchsia, 0.0)) { g.FillRectangle(gradient, r); } g.DrawRectangle(Pens.Black, r); StringFormat fmt = new StringFormat(StringFormatFlags.NoWrap); fmt.LineAlignment = StringAlignment.Center; fmt.Trimming = StringTrimming.EllipsisCharacter; fmt.Alignment = StringAlignment.Near; g.DrawString(this.GetText(), this.ListView.Font, Brushes.Black, r, fmt); return true; } }
Notice that here we've overridden a different base method: OptionalRender()
. This method allows the renderer to decide if it wants to do the rendering itself or to fall back on the default rendering. Returning true says that the rendering has been done and no further processing is needed.
To see an extreme example of owner drawing in a non-detail view, go to the Complex tab of the ObjectListView demo, turn on Owner Drawn and switch to Tile view. There, you should be able to see something like this (which is probably taking owner drawing to places it really does not want to go):
OwnerDrawn
mode. So, you can only see your custom renderer when the ObjectListView
is in owner-drawn mode. [I spent one very frustrating hour during the development of this code because I forgot about this]. ObjectListView
are always of fixed height. Row height can now be set for the whole ObjectListView
using the RowHeight
property, but it cannot be changed for individual rows.