I have a ListView where I wish to tweak the drawing of items (for example highlighting certain strings in list view itmes), however I don't want to radically alter the way that items are displayed.
I have set the OwnerDraw to true and can get my head around how to draw my highlighting effect, however whenever I try to defer to the default implementation to draw the rest of the list view item things go wrong and I'm left with a whole load of graphical problems indicating that actually I've completely gone wrong.
Is there somewhere that I can see what the "Default" handlers for the DrawItem and DrawSubItem events do so that I can better my understanding and more easily tweak my code?
For reference here is a snippet showing what I'm currently doing:
public MyListView()
{
this.OwnerDraw = true;
this.DoubleBuffered = true;
this.DrawColumnHeader += new DrawListViewColumnHeaderEventHandler(MyListView_DrawColumnHeader);
this.DrawItem += new DrawListViewItemEventHandler(MyListView_DrawItem);
this.DrawSubItem += new DrawListViewSubItemEventHandler(MyListView_DrawSubItem);
}
private void MyListView_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
{
// Not interested in changing the way columns are drawn - this works fine
e.DrawDefault = true;
}
private void MyListView_DrawItem(object sender, DrawListViewItemEventArgs e)
{
e.DrawBackground();
e.DrawFocusRectangle();
}
private void MyListView_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
{
string searchTerm = "Term";
int index = e.SubItem.Text.IndexOf(searchTerm);
if (index >= 0)
{
string sBefore = e.SubItem.Text.Substring(0, index);
Size bounds = new Size(e.Bounds.Width, e.Bounds.Height);
Size s1 = TextRenderer.MeasureText(e.Graphics, sBefore, this.Font, bounds);
Size s2 = TextRenderer.MeasureText(e.Graphics, searchTerm, this.Font, bounds);
Rectangle rect = new Rectangle(e.Bounds.X + s1.Width, e.Bounds.Y, s2.Width, e.Bounds.Height);
e.Graphics.FillRectangle(new SolidBrush(Color.Yellow), rect);
}
e.DrawText();
}
I haven't got the time now to write up a complete answer so instead I'll put down some quick notes and come back to it later.
As LarsTech said, owner drawing a ListView control is a pain - the .Net ListView class is a wrapper around the underlying Win32 List View Control and the ability to "Owner draw" is provided by the NM_CUSTOMDRAW notification code. As such there is no "default .Net implementation" - the default is to use the underlying Win32 control.
To make life even more difficult there are a number of extra considerations to make:
As LarsTech pointed out, the first subitem in fact represents the parent item itself, and so if you handle rendering in both DrawItem and DrawSubItem you may well be drawing the contents of the first cell twice.
There is a bug in the underlying list view control (documented on the note on this page) that means that a DrawItem event will occur without corresponding DrawSubItem events, meaning that if you draw a background in the DrawItem event and then draw the text in the DrawSubItem event your item text will disappear when you mouse over.
Some of the rendering also appears to not be double-buffered by default
I also noticed that the ItemState property is not always correct, for example just after resizing a column. Consequently I've found its best not to rely on it.
You also need to make sure that your text doesn't split over multiple lines, else you will see the top few pixels of the lower line being rendered at the bottom of the cell.
Also special consideration needs to be given when rendering the first cell to take account of extra padding that the native list view uses.
Because the DrawItem event occurs first, anything you draw in the DrawItem handler (e.g. the selection effect) may well be overlayed by things you do in the DrawSubItem handler (e.g. having certain cells with a different background color).
All in all handling owner drawing is a fairly involved affair - I found it best to handle all drawing inside the DrawSubItem event, its also best to perform your own double-buffering by using the BufferedGraphics class.
I also found looking at the source code for ObjectListView very handy.
Finally, all of this is just to handle the details mode of the list view (the only mode I am using), if you want the other modes to work too then I believe that there are extra things to take account of.
When I get a chance I'll try and post my working example code.
I don't know if this will completely help you, but I'll add a few notes:
One thing to keep in mind is that DrawSubItem will draw the first item, too, and that's probably where you are getting the double-rendered look from.
Some things to try (not factored for speed):
private void listView1_DrawItem(object sender, DrawListViewItemEventArgs e) {
e.DrawBackground();
if ((e.State & ListViewItemStates.Selected) == ListViewItemStates.Selected) {
Rectangle r = new Rectangle(e.Bounds.Left + 4, e.Bounds.Top, TextRenderer.MeasureText(e.Item.Text, e.Item.Font).Width, e.Bounds.Height);
e.Graphics.FillRectangle(SystemBrushes.Highlight, r);
e.Item.ForeColor = SystemColors.HighlightText;
} else {
e.Item.ForeColor = SystemColors.WindowText;
}
e.DrawText();
e.DrawFocusRectangle();
}
For your DrawSubItem routine, make sure you aren't drawing in the first column and I added the DrawBackground() routine. I added some clipping to the highlight rectangle so it wouldn't paint outside the column parameters.
private void listView1_DrawSubItem(object sender, DrawListViewSubItemEventArgs e) {
if (e.ColumnIndex > 0) {
e.DrawBackground();
string searchTerm = "Term";
int index = e.SubItem.Text.IndexOf(searchTerm);
if (index >= 0) {
string sBefore = e.SubItem.Text.Substring(0, index);
Size bounds = new Size(e.Bounds.Width, e.Bounds.Height);
Size s1 = TextRenderer.MeasureText(e.Graphics, sBefore, this.Font, bounds);
Size s2 = TextRenderer.MeasureText(e.Graphics, searchTerm, this.Font, bounds);
Rectangle rect = new Rectangle(e.Bounds.X + s1.Width, e.Bounds.Y, s2.Width, e.Bounds.Height);
e.Graphics.SetClip(e.Bounds);
e.Graphics.FillRectangle(new SolidBrush(Color.Yellow), rect);
e.Graphics.ResetClip();
}
e.DrawText();
}
}
In general, owner drawing a ListView control is welcoming in a world of hurt. You aren't drawing in Visual Styles anymore, you would have to do that yourself, too. Ugh.
Item selected back color changed. In by default blue in windows. This code will help for u in any colors:
private void listView1_DrawItem(object sender, DrawListViewItemEventArgs e)
{
e.DrawBackground();
if (e.Item.Selected)
{
e.Graphics.FillRectangle(new SolidBrush(Color.FromArgb(250, 194, 87)), e.Bounds);
}
e.Graphics.DrawString(e.Item.Text, new Font("Arial", 10), new SolidBrush(Color.Black), e.Bounds);
}
private void Form1_Load(object sender, EventArgs e)
{
for (int ix = 0; ix < listView1.Items.Count; ++ix)
{
var item = listView1.Items[ix];
item.BackColor = (ix % 2 == 0) ? Color.Gray : Color.LightGray;
}
}
private void listView1_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
{
e.DrawDefault = true;
}
}
}
ComponentOwl recently released a freeware component called Better ListView Express.
It looks and behaves exactly like the ListView, but has much more powerful owner drawing capabilities - you can draw accurately over all elements and even turn off some drawing (e.g. selection to make you on).
private void listView1_DrawItem(object sender, DrawListViewItemEventArgs e)
{
e.DrawBackground();
if (e.Item.Selected)
{
e.Graphics.FillRectangle(new SolidBrush(Color.FromArgb(250, 194, 87)), e.Bounds);
}
e.Graphics.DrawString(e.Item.Text, new Font("Arial", 10), new SolidBrush(Color.Black), e.Bounds);
}
Related
When I increase the font size in my ListBox, the larger text strings get vertically clipped because the item draw rectangle size (spacing) does not increase with the font size. Each lower line of text overlays the previous line of text so that only the tops of letters show in larger font sizes.
Everything works fine with DrawMode.OwnerDrawFixed if I don't change the font sizes. Smaller-than-normal fonts display properly. It's only the larger fonts that get clipped. I changed to DrawMode.OwnerDrawVariable and added a MeasureItem event handler to measure the larger font size explicitly, but that did not work either.
What am I missing? Thank you.
UPDATE: I provided working code below based on #Jimi's examples in the links he provided in his comments. I was missing the idea that the MeasureItem event runs only once (or when necessary) and not for each drawn item. Thus, you must manually trigger a new MeasureItem event when you change the font size in the ListBox. The trick to generating the event is to flip the DrawMode.OwnerDrawVariable to Normal and back.
listBox1.DrawMode = DrawMode.OwnerDrawVariable;
listBox1.DrawItem += DrawItemHandler;
listBox1.MeasureItem += MeasureItemHandler;
private void MeasureItemHandler(object sender, MeasureItemEventArgs e) {
// must measure the height to allow for user font size changes
var listBox = (ListBox) sender;
e.ItemHeight = listBox.Font.Height;
}
public void DrawItemHandler(object sender, DrawItemEventArgs e) {
// prepare text and colors for drawing here
//
using (var bgbrush = new SolidBrush(bgcolor)) {
using (var fgbrush = new SolidBrush(fgcolor)) {
e.Graphics.FillRectangle(bgbrush, e.Bounds);
e.Graphics.DrawString(logEntry.EntryText, e.Font, fgbrush, e.Bounds);
}
}
e.DrawFocusRectangle();
}
private void fontUpToolStripMenuItem_Click(object sender, EventArgs e) {
// To increase size alone, use the original font family
var size = listBox1.Font.Size;
if (size < 32) {
// set the new font size, register it with the list box, and then
// flip the draw mode back and forth to trigger a MeasureItem event.
// The measure event will recalculate the bounds for all items in the box.
listBox1.Font = new Font(listBox1.Font.FontFamily, size + 1);
listBox1.Height = listBox1.Font.Height;
listBox1.DrawMode = DrawMode.Normal;
listBox1.DrawMode = DrawMode.OwnerDrawVariable;
listBox1.Invalidate();
}
}
I am trying to replicate the following drag and drop functionality:
However, I am having trouble drawing that black line. I end up, somehow, with three lines per node. I calculate the upper, middle and lower part of the rectangle containing the tree node and, depending on that, i draw one of the lines. If I do use treeView.Invalidate() the screens flickers too much and the line cannot be seen. I have also tried to used graphics.clear(treeView.BackColor) but it will also clear my tree nodes.
Demo:
Code - tree events:
public void ItemDrag(object sender, ItemDragEventArgs e)
{
TreeNode selectedNode = (TreeNode)e.Item;
if (e.Button == MouseButtons.Left && !selectedNode.Name.Contains("=") && !selectedNode.Name.Contains("#"))
_treeView.DoDragDrop(selectedNode, DragDropEffects.Move);
}
public void DragEnter(object sender, DragEventArgs e)
{
e.Effect = e.AllowedEffect;
}
public void DragOver(object sender, DragEventArgs e)
{
try
{
if (!mousePoint.Equals(Cursor.Position))
{
mousePoint = Cursor.Position;
bool droppable;
TreeNode destinationNode = null;
Point pointInTree = _treeView.PointToClient(new Point(e.X, e.Y));
if (e.Data.GetDataPresent(typeof(TreeNode)))
{
destinationNode = _treeView.GetNodeAt(pointInTree);
TreeNode souceNode = (TreeNode) e.Data.GetData(typeof(TreeNode));
droppable = true;
}
else droppable = false;
e.Effect = droppable ? DragDropEffects.Move : DragDropEffects.None;
Point pt = _treeView.PointToClient(new Point(e.X, e.Y));
_treeView.SelectedNode = _treeView.GetNodeAt(pt);
int dropLocation = CalculateNodeHooverArea(destinationNode, pointInTree);
if(_dropLocation!=dropLocation)
{
switch (dropLocation)
{
case 0:
DrawLine(NodePosition.Above);
break;
case 2:
DrawLine(NodePosition.Below);
break;
case 1:
DrawLine(NodePosition.In);
break;
}
_dropLocation = dropLocation;
}
}
}
catch (Exception exception)
{
Debug.WriteLine("TreeViewDragOverEvent: " + exception.Message);
}
}
public void DragDrop(object sender, DragEventArgs e)
{
try
{
Point targetPoint = _treeView.PointToClient(new Point(e.X, e.Y));
TreeNode targetNode = _treeView.GetNodeAt(targetPoint);
TreeNode draggedNode = (TreeNode)e.Data.GetData(typeof(TreeNode));
if (!draggedNode.Equals(targetNode) && !draggedNode.Nodes.Find(targetNode.Name, true).Any() &&
targetNode.Parent != null && !targetNode.Name.Contains("=") && !targetNode.Name.Contains("#"))
{
int nodeLocation = CalculateNodeHooverArea(targetNode, targetPoint);
if (e.Effect == DragDropEffects.Move)
draggedNode.Remove();
switch (nodeLocation)
{
case 0:
if (targetNode.Parent != null)
targetNode.Parent.Nodes.Insert(targetNode.Index, draggedNode);
break;
case 1:
targetNode.Nodes.Add(draggedNode);
break;
case 2:
if (targetNode.Parent != null)
targetNode.Parent.Nodes.Insert(targetNode.Index + 1, draggedNode);
break;
}
}
//SaveMemento();
_treeView.Invalidate();
}
catch (Exception exception)
{
Debug.WriteLine(exception.Message);
}
}
The method that handles the drawing of the line:
private void DrawLine(NodePosition position)
{
Graphics g = _treeView.CreateGraphics();
Pen customPen = new Pen(Color.DimGray, 1) { DashStyle = DashStyle.Dash };
if (position == NodePosition.Above)
g.DrawLine(customPen, new Point(0, _treeView.SelectedNode.Bounds.Top),
new Point(_treeView.Width - 4, _treeView.SelectedNode.Bounds.Top));
else if (position == NodePosition.Below)
g.DrawLine(customPen, new Point(0, _treeView.SelectedNode.Bounds.Bottom),
new Point(_treeView.Width - 4, _treeView.SelectedNode.Bounds.Bottom));
else
{
g.DrawLine(customPen, new Point(_treeView.SelectedNode.Bounds.X + _treeView.SelectedNode.Bounds.Width,
_treeView.SelectedNode.Bounds.Y +_treeView.SelectedNode.Bounds.Height / 2),
new Point(_treeView.Width - 4, _treeView.SelectedNode.Bounds.Y + _treeView.SelectedNode.Bounds.Height / 2));
}
customPen.Dispose();
g.Dispose();
}
Can this be solved somehow or shall I look into a different way of displaying that sort of information? (e.g. tool tip)?
You really don't have to draw the line every time you reach near the new TreeNode. In my view, it's not a clean solution to keep using Graphics object to draw and erase something every time on drag/drop event. Instead, Here's what I call a lot more 'cleaner' way of achieving the same-
On your Windows Form, draw a line as a static control and set its visibility to false initially. Now how do you draw a line? Add a label control, add a solid or 3D border, clear the text, and set a fixed height - may be 2 pixels, and a width as needed. Place this label somewhere on bottom-left corner of the form, where it doesn't come in a way of other controls on UI.
Instead of DrawLine method, call it ShowLine or something. In this method, dynamically set the X and Y position of this new label (a line, actually) to the new location as per the position of TreeView node, and make it visible. So, every time on DragOver it would be visible on a different X and Y position, and give you the same experience as you need.
Once the item is dropped inside a node (i.e., drag-n-drop operation is complete), set the visibility of this line label to false, and also set its X and Y position back to the original one (bottom-left corner in this case).
Invalidate() is the correct method to call when you want your control to be redrawn without performing layouting algorithms.
The problem seems to be that you use a new Graphics object by calling _treeView.CreateGraphics().
You could try to just invalidate a calculated region you want to update (where the old separator lines were drawn) or alternatively use an double-buffered approach I would rather use for fully custom drawn controls but it might be worth a try: Remove OnPaintBackground() update that won't work without overriding the painting of the control in total.
As I think about that issue ... why don't you store the coords of the last line and as soon as another line gets drawn, "erase" (overpaint) it again with the same pen size and shape but with the color of the background. I know, this reads to be a bit dirty but in the end it's all about unnoticable and well performing hacks (thinking about render tricks in game engines for example). As soon as the user scrolls or does anything else that will make the overpainting unsuccessfull because the coords did change, you won't need to overpaint it either because the control as whole gets repainted by the OS.
I'm sure this is something very easy to figure out but I cannot do it. I have a winform with 3 Label inside a Panel. When the form loads, the first Label has a Paint event that draws a rectangle on it. I would like a backgroundWorker to go through each one, wait 5 seconds, restore the Label to normal (redrawing I'm guessing) and then draw a rectangle on the following Label.
public List<Label> GetLabelList()
{
return new List<List>()
{
label1,
label2,
label3,
label4
};
}
private void bgBackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
var getList = GetLabelList();
for (int i = 0; i < getList.Count; i++)
{
if ((bgBackgroundWorker.CancellationPending == true))
{
e.Cancel = true;
break;
}
else
{
Thread.Sleep(5000);
getList [i].Paint += RemoveLabelHighlight;
getList [i].Invalidate();
if (i < 2)
{
getList [i + 1].Paint += AddLabelHighlight;
getList [i + 1].Invalidate();
}
bgBackgroundWorker.ReportProgress((i * 10));
}
}
}
private void AddLabelHighlight(object sender, PaintEventArgs e)
{
var label = sender as Label;
e.Graphics.DrawRectangle(new Pen(Color.DeepPink, 8), label.ClientRectangle);
}
private void RemoveLabelHighlight(object sender, PaintEventArgs e)
{
var label = sender as Label;
e.Graphics.DrawRectangle(new Pen(Color.Green, 8), label.ClientRectangle); // This should return the Label back to original state
}
This works but when the rectangle is drawn, the label is cut off all the way around. Any suggestions?
Also, I'm sure there is a much better and more efficient way to achieve this, maybe by an EventHandler or something. I'd like some suggestions, if possible.
This is actually being caused by your use of the pen width of 8 pixels, I believe. Try a different size and see if that changes the size of the rectangle not being drawn.
To fill the rectangle instead, use:
e.Graphics.FillRectangle(new SolidBrush(Color.DeepPink), e.ClipRectangle);
EDIT Since you're now completely responsible for drawing the control, the text can be redrawn with a DrawString call:
e.Graphics.DrawString(label.Text, label.Font, SystemBrushes.ControlText, new PointF(0,0));
EDIT Here's how to nest a panel and a label to achieve what you're looking for:
Add a new panel, set the padding to 8,8,8,8, and BackColor to whatever you like
Add a new label to this panel, set it's AutoSize property to false, Dock property to Fill, and TextAlign property to MiddleCenter
While I have always loved doing owner-drawn stuff, sometimes it's just easier to use what's there! For fun though, I would wrap this into a new Panel-derived control to make it easy to reuse.
I'm drawing custom ListView, with OwnerDraw property set to 'true'. Also listview has AllowColumnReorder 'true' property.
private void listView1_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
{
e.Graphics.DrawString(e.SubItem.Text, Font, Brushes.Black, e.Bounds);
}
This is working fine:
But if I move the first column there is a drawing problem - data from first two columns is painted in frist column, and the data from the column which was moved is not painted at all:
This happens because e.Bounds has equals values for two differents columns. What can I do to get correct e.Bounds value.
Yes, this is a bug in ListView class. Its private GetItemRectOrEmpty() method is borken. Written as a bug workaround, internal bug number VSWhidbey #163674. Having a bug fix cause another bug is a pretty traditional programming mishap, the big boys make them too :) When it asks Windows for the item rectangle, passed to you through the e.Bounds property, it flubs and asks for ItemBoundsPortion.Entire. Which is the complete ListViewItem rectangle, including the subitems.
Luckily the workaround is simple, you can use ItemBoundsPortion.ItemOnly yourself:
private void listView1_DrawSubItem(object sender, DrawListViewSubItemEventArgs e) {
var bounds = e.Bounds;
if (e.ColumnIndex == 0) {
bounds = listView1.GetItemRect(e.ItemIndex, ItemBoundsPortion.ItemOnly);
}
e.Graphics.DrawString(e.SubItem.Text, Font, Brushes.Black, bounds);
}
Thank Hans Passant for info. I fixed this bug using next code:
private void listView1_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
{
Rectangle bounds = e.Bounds;
if (e.ColumnIndex == 0 && listView1.Columns[0].DisplayIndex != 0)
{
bounds = GetFirstColumnCorrectRectangle(e.Item);
}
e.Graphics.DrawString(e.SubItem.Text, Font, Brushes.Black, bounds);
}
private Rectangle GetFirstColumnCorrectRectangle(ListViewItem item)
{
int i;
for (i = 0; i < listView1.Columns.Count; i++)
if (listView1.Columns[i].DisplayIndex == listView1.Columns[0].DisplayIndex - 1)
break;
return new Rectangle(item.SubItems[i].Bounds.Right, item.SubItems[i].Bounds.Y, listView1.Columns[0].Width, item.SubItems[i].Bounds.Height);
}
I'm developing an application for kind of touch screen device. In order be user friendly, I need to change size of combobox.
I've checked many thing including DrawItemEventHandler and MeasureItemEventHandler, but it didn't work as I want.
Basically I would like to change height of combobox without touching font size. When I change font size of combobox, it looks like left side of the image.
How can I set my combobox which will look like right side of the image?
By the way, don't know if it's effect solution, I am not using array string. I'm binding data like.
combobox.DisplayMember = "Name";
combobox.ValueMember = "ID";
combobox.DataSource = new BindingSource { DataSource = datalist };
Thanks in advance.
With TaW solution, I managed to set items as I want. The only thing I couldn't set text in middle when combobox items not droped down. How can I set this text position to the centre?
You can set the ItemHeight property and then draw the items yourself in the DrawItem event.
Not terribly hard, search for 'ownerdraw' & 'combobox'. There is one example on Code Project
Here is a minimal version, pulled from the above link:
private void comboBox1_DrawItem(object sender, DrawItemEventArgs e)
{
if (e.Index < 0) return;
Font f = comboBox1.Font;
int yOffset = 10;
if ((e.State & DrawItemState.Focus) == 0)
{
e.Graphics.FillRectangle(Brushes.White, e.Bounds);
e.Graphics.DrawString(comboBox1.Items[e.Index].ToString(), f, Brushes.Black,
new Point(e.Bounds.X, e.Bounds.Y + yOffset));
}
else
{
e.Graphics.FillRectangle(Brushes.Blue, e.Bounds);
e.Graphics.DrawString(comboBox1.Items[e.Index].ToString(), f, Brushes.White,
new Point(e.Bounds.X, e.Bounds.Y + yOffset));
}
}
You also have to set the DropDownStyle to DropDownList to get the highlighting to work and you need to set the DrawMode to OwnerDrawFixed. (Or to OwnerDrawVariable, if you want to have different heights for some itmes..)