I have created a custom ListView control to suit my needs and I'm having an issue that causes the ListView not to show any contents (not drawing anything, just white) when the form first loads.
If I resize the form or click on my control (anything that forces a repaint on the ListView) then it shows up as expected.
As a side note, it used to work just fine until I made a small change today and rebuilt the control. I removed all the changes I made and rebuilt again but the issue still happens. Any ideas as to why it will not show up (paint) when the form is first loaded?
This is what I use to do the custom drawing on my custom ListView control...
protected override void OnDrawItem(DrawListViewItemEventArgs e)
{
Image image = e.Item.ImageList.Images[e.Item.ImageIndex];
Size textSize = new Size((int)e.Graphics.MeasureString(e.Item.Text, e.Item.Font).Width, (int)e.Graphics.MeasureString(e.Item.Text, e.Item.Font).Height);
//Get the area of the item to be painted
Rectangle bounds = e.Bounds;
bounds.X = 0;
bounds.Width = this.Width;
//Set the spacing on the list view items
int hPadding = 0;
int vPadding = 0;
IntPtr padding = (IntPtr)(int)(((ushort)(hPadding + bounds.Width)) | (uint)((vPadding + bounds.Height) << 16));
SendMessage(this.Handle, (uint)ListViewMessage.LVM_SETICONSPACING, IntPtr.Zero, padding);
//Set the positions of the image and text
int imageLeft = (bounds.Width / 2) - (image.Width / 2);
int imageTop = bounds.Top + 3;
int textLeft = (bounds.Width / 2) - (textSize.Width / 2);
int textTop = imageTop + image.Height;
Point imagePosition = new Point(imageLeft, imageTop);
Point textPosition = new Point(textLeft, textTop);
//Draw background
using (Brush brush = new SolidBrush(e.Item.BackColor))
e.Graphics.FillRectangle(brush, bounds);
//Draw selected
if (e.Item.Selected)
{
using (Brush brush = new SolidBrush(m_SelectedColor))
e.Graphics.FillRectangle(brush, bounds);
}
//Draw image
e.Graphics.DrawImage(image, imagePosition);
//Draw text
e.Graphics.DrawString(e.Item.Text, e.Item.Font, new SolidBrush(e.Item.ForeColor), textPosition);
}
I also set the following things in the Constructor of my custom control...
public MyListView()
{
this.DoubleBuffered = true;
this.OwnerDraw = true;
this.View = View.LargeIcon;
this.Cursor = Cursors.Hand;
this.Scrollable = false;
}
I also inherit the ListView class...
public class MyListView : ListView
{
//All my source
}
You need to set the set the control to redraw itself when resized. So add this code in constructor of your control:
this.ResizeRedraw = true;
My apologies:
Doing the below reset my event handlers and the problem went away. However, once I hooked them up, I found the Resize event handler that I used to set the ColumnWidth was causing the issue. Why setting the ColumnWidth is causing this, I do not know.
Arvo Bowen's comment on this answer fixed it for me also (.NET 4.8 Framework, VS2022). To be clear, not requiring this.ResizeRedraw = true; from the answer.
So after much headache and time I found that I had absolutely nothing wrong with my control. It was your answer that made me create another control just like my existing one and test it. To my surprise it worked great. I simply copied my existing non-working control and pasted it on the form and then new one worked! Sometimes VS just does weird things... Somehow I managed to muck up the control's creation object or something..
Related
I am using Microsoft Visual Studio Community 2017 version 15.7.2, and .NET Framework version 4.7.03056.
I am using the Winforms TreeView and am modifying its default behavior to make it a little bit more like the Windows Explorer tree view. I set the following properties:
LineHeight` 22
DrawMode OwnerDrawAll
I am using the following for the DrawNode event. This code uses right and down bracket bitmaps (which are 16x16) to show expanded or unexpanded nodes, and uses custom colors for select/focus highlighting. Nothing exotic.
private void treeDir_DrawNode(object sender, DrawTreeNodeEventArgs e)
{
const int indent = 12;
const int markerSpacing = 20;
int leftPos = e.Bounds.Left + e.Node.Level * indent;
Brush selectBrush;
Pen pen;
Graphics g = e.Graphics;
e.DrawDefault = false;
if (e.Node.IsSelected)
{
if (e.Node.TreeView.Focused)
{
selectBrush = new SolidBrush(FocusedBackgroundColor);
pen = new Pen(new SolidBrush(FocusedPenColor));
}
else
{
selectBrush = new SolidBrush(UnfocusedBackgroundColor);
pen = new Pen(new SolidBrush(UnfocusedPenColor));
}
g.FillRectangle(selectBrush, e.Bounds);
g.DrawRectangle(pen, e.Bounds);
}
if (e.Node.Nodes.Count > 0)
{
if (e.Node.IsExpanded)
{
g.DrawImage(Properties.Resources.Expanded, leftPos+2, e.Bounds.Top+2);
}
else
{
g.DrawImage(Properties.Resources.Unexpanded, leftPos+2, e.Bounds.Top+2);
}
}
g.DrawString(
e.Node.Text, CommonFont, new SolidBrush(Color.Black), leftPos + markerSpacing, e.Bounds.Top+2);
}
What's happening is that when the form is first shown, if I expand a node that is not the first node, it also overwrites (transparently overlays) the first node text. Here's the sequence.
On start up of the form:
Then I double click Node 4:
If I double click the first node, the problem clears up:
From this point forward, if I double click Node 4, the problem no longer occurs. Double clicking the first node clears up the problem and avoids it for the life of the form after that point for Node 4. However, if I double click another expandable node further down, it happens again.
Is this a bug in TreeView or am I doing something incorrect in my owner draw?
The DrawNode event is called rather too often when doubleclicking and one set of calls has a bounds rectangle that is Empty.
(Maybe the reasoning was: If all drawing is happening only in the empty rectangle nothing will show. Hm..)
So as a workaround you can shortcut the DrawNode event for all the wrong calls at the beginning of the event:
if (e.Bounds.Height < 1 || e.Bounds.Width < 1) return;
I also recommend text rendering like this..:
TextRenderer.DrawText(g, e.Node.Text, CommonFont,
new Point( leftPos + markerSpacing, e.Bounds.Top+2), Color.Black);
TextRenderer is always recommended over Graphics.DrawString for forms as it improves on several shortcomings.
I'm working on a winforms application using MetroFramework. I want to programmatically resize the form so a larger object can fit on it. For this I'm using a toggle from the framework in a tabControl object. Making the form bigger works fine, but when I disable the toggle it doesn't want to shrink the form.
private void tSynced_CheckedChanged(object sender, EventArgs e)
{
if (tSynced.Checked)
{
//Sync enabled
Console.WriteLine("Sync enabled");
this.Size = new Size(this.Width + 300, this.Height);
this.MinimumSize = new Size(this.Width, this.Height);
this.MaximumSize = new Size(this.Width + 200, this.Height);
} else
{
//Sync disabled
Console.WriteLine("Sync disabled");
this.Size = new Size(this.Width - 300, this.Height);
this.Width = 534;
Console.WriteLine(this.Size);
this.MinimumSize = new Size(this.Width, this.Height);
this.MaximumSize = new Size(this.Width, this.Height);
}
}
As you can see I've been experimenting with some techniques that I know, just to give it a try and work my way back from there, but it doesn't seem to work. This seems odd to me, since the first method (the this.Size line) does work when making the form bigger. The this keyword is referring to the Form according to Visual Studio. It doesn't seem to try to resize the tabControl, because I bound that to the right side, and properly moves along with the right border.
The log for the this.Size will return the large value, which is {Width=834, Height=354} in my case.
I've tried saving the initial values in a Size variable, and restoring it from there (since it would account for resizing by the user), but that doesn't seem to work correctly.
Size oldSize; //Global variable
private void initialize()
{
oldSize = new Size(this.Width, this.Height);
Console.WriteLine(oldSize);
}
this.Size = oldSize; //In the eventhandler
The log will return the correct value, which is {Width=534, Height=354} in my case. But it refuses to use the value when setting the this.Size property again...
What am I overlooking?
When enlarging the form, you set the MinimumSize to the current size - but when you make it smaller you try to reset the Size while the MinimumSize is still set - so the form doesn't resize.
All you need to do is to reset the MinimumSize before changing the Size.
Basically, I've created an extension of the panel class that adds draws multiple bitmaps onto itself In order to create multiple musical staves. I've tried adding a vertical scroll bar onto the panel but that hasn't worked. My Paint procedure is similar to this
private void StavePanel_Paint(object sender, PaintEventArgs e)
{
for(int i = 0; i < linenumber; i++)
{
Bitmap bmp = new Bitmap(Width, 200);
//edit bmp to resemble stave
e.Graphics.DrawImage(bmp,new Point(0,200*i);
}
}
Just set the AutoScrollMinSize property:
panel1.AutoScrollMinSize = new Size(0, 1000);
During the paint event, you need to translate the positions of your drawing by using the TranslateTransform method. Also, you need to dispose your bitmaps after you draw them:
e.Graphics.TranslateTransform(panel1.AutoScrollPosition.X, panel1.AutoScrollPosition.Y);
using (Bitmap bmp = new Bitmap(Width, 200)) {
//edit bmp to resemble stave
e.Graphics.DrawImage(bmp,new Point(0,200*i);
}
or create and store them ahead of time to avoid that cost during the paint event.
Set the AutoScroll property to true.
You might also consider alternatives:
FlowLayoutPanel and add PictureBoxes dynamically instead of painting.
TableLayoutPanel and add PictureBoxes dynamically instead of painting.
extend ListBox and set the DrawMode property to OwnerDrawFixed or OwnerDrawVariable and then override the methods OnPaint and OnMeasureItem (only for OwnerDrawVariable).
If you want to continue using your existing pattern of calling GDI code to paint your control you should add a scrollbar control and add an event handler to its change event. The change handler doesn't need to do anything other than call .Invalidate on the panel. .Invalidate is a signal to the control that it is "dirty" and needs to be redrawn. You will need to modify your painting code to offset the drawing in the inverse direction of the scrollbar value.
So if your scrollbar is at position 50, you should draw everything at Y - 50.
If you are using pure GDI drawing code there is no need to mess with the AutoScroll property at all. That is only used if your panel hosts an actual control that is larger than the panel.
As others mentioned, you need to set AutoScroll to true. But then, anytime you add or remove a bitmap (or at the beginning if they are fixed), you need to set the AutoScrollMinSize height using the formula bitmapCount * bitmapHeight. Also in your paint handler you need to consider the AutoScrollPosition.Y property.
Here is a small example of the concept in action:
using System;
using System.Drawing;
using System.Windows.Forms;
namespace Tests
{
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var form = new Form();
var panel = new Panel { Dock = DockStyle.Fill, Parent = form };
// Setting the AutoScrollMinSize
int bitmapCount = 10;
int bitmapHeight = 200;
panel.AutoScrollMinSize = new Size(0, bitmapCount * bitmapHeight);
panel.Paint += (sender, e) =>
{
// Considering the AutoScrollPosition.Y
int offsetY = panel.AutoScrollPosition.Y;
var state = offsetY != 0 ? e.Graphics.Save() : null;
if (offsetY != 0) e.Graphics.TranslateTransform(0, offsetY);
var rect = new Rectangle(0, 0, panel.ClientSize.Width, bitmapHeight);
var sf = new StringFormat(StringFormat.GenericTypographic) { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
for (int i = 0; i < bitmapCount; i++)
{
// Your bitmap drawing goes here
e.Graphics.FillRectangle(Brushes.Yellow, rect);
e.Graphics.DrawRectangle(Pens.Red, rect);
e.Graphics.DrawString("Bitmap #" + (i + 1), panel.Font, Brushes.Blue, rect, sf);
rect.Y += bitmapHeight;
}
if (state != null) e.Graphics.Restore(state);
};
Application.Run(form);
}
}
}
EDIT: As LarsTech correctly mentioned in the comments, you don't really need to set AutoScroll property in this case. All other remains the same.
I have a listview that uses custom cell colors, but when I set a background image in the listview, the custom cell colors will not appear anymore. I tried to remove the background image temporarily (when assembling the list) and restore it after applying cell colors. This results in no custom colors but shows the background. I would like to combine these 2 listview properties if possible.
My code for setting/removing background image:
list.BackgroundImage = Properties.Resources.bgalpha;
list.BackgroundImage = null;
A part of my code for setting custom cell colors:
for (int i = 0; i < kavels.Count(); i++ )
{
if (list.Items[i].SubItems[1].Text != "0")
{
list.Items[i].UseItemStyleForSubItems = false;
list.Items[i].SubItems[1].BackColor = Color.LightGreen;
}
}
Here are two screenshots:
List view with background: http://i.imgur.com/aHUXAVh.png
List view without background: http://i.imgur.com/sO83wTP.png
I also tried making a PictureBox with a transparent background along with a png image with transparency on top of the ListView, but that also didn't work obviously.
You have two options:
You could overlay a Panel or a PictureBox with a semi-transprent Image. For this to work you would have to make it sit inside the ListView, so that it is the Parent of the overlay.
But that will make the Listview non-clickable. - Another problem with this is that it will slightly color the text, so it won't look quite right.
Or you can set the ListView to OwnerDraw = true and add code to do the drawing yourself.
Here is an example, non-scrolled and scrolled:
Note that the original BackgroundImage shines through the emtpy space to the right.
If you owner-draw a ListView in Details mode you need to code events to draw subitems and headers; note the class level variable to hold the itemHeight; this assumes they all have the same Height .. The other one is need for horizontal scrolling.
int itemHeight = 0; // we need this number!
int itemLeft = 0; // we need this number, too
private void listView1_DrawColumnHeader(object sender,
DrawListViewColumnHeaderEventArgs e)
{
Rectangle R0 = listView1.GetItemRect(0);
itemHeight = R0.Height; // we need this number!
itemLeft = R0.Left; // we need this number too
e.DrawBackground();
e.DrawText();
}
private void listView1_DrawSubItem(object sender,
DrawListViewSubItemEventArgs e)
{
Rectangle rrr = listView1.GetItemRect(e.ItemIndex);
Rectangle rect = e.Bounds;
Rectangle rect0 = new Rectangle(rect.X - itemLeft , itemHeight * e.ItemIndex,
rect.Width, rect.Height);
Image img = listView1.BackgroundImage;
e.Graphics.DrawImage(img, rect, rect0, GraphicsUnit.Pixel);
using (SolidBrush brush = new SolidBrush(e.SubItem.BackColor))
e.Graphics.FillRectangle(brush, rect);
e.DrawText();
}
Here is the code to set the colors in the ListViewItem lvi for the example:
lvi.UseItemStyleForSubItems = false;
lvi.BackColor = Color.FromArgb(66, Color.LightBlue);
lvi.SubItems[1].BackColor = Color.FromArgb(77, Color.LightGreen);
lvi.SubItems[2].BackColor = Color.FromArgb(88, Color.LightPink);
Note that the code assumes your background is one large image and no tiling is involved! Also the code works only if you don't have groups!
ObjectListView -- an open source wrapper around a standard .NET ListView -- provides ImageOverlays and true background images too. They both work with colour cells.
I have a userControl which has some programmatically drawn rectangles. I need few instances of that userControl on my form (see the image). The problem is that only the last instance will show the drawn shapes!
I guess it has something to do with drawing surface or the Paint event handler
In case it might help, here's some of the code I use in my control:
private void MyUserControl_Paint(object sender, PaintEventArgs e)
{
showHoraireMaitresse();
Rectangle rec = showDisponibilités();
var b = new SolidBrush(Color.FromArgb(150, Color.Blue));
e.Graphics.FillRectangle (b, rec);
showOccupation();
}
private void showHoraireMaitresse()
{
heureDebut = 8;
for (int i = 0; i < 14; i++)
{
//Label d'heure -> This shows just fine
addLabel(i, heureDebut);
//Rectangles d'heure -> This shows only in last instance
var rectangle = new Rectangle(180 + i * largeurDUneHeure, 14, largeurDUneHeure, 30);
surface.DrawRectangle(defaultPen, rectangle);
}
addLabel(14, heureDebut);
}
Thank you!
Without further information, I'm going to guess that 'surface' is static.
Trace through OnPaint and check which control is painting, and what the bounds are for 'surface'. Perhaps all the controls are painting the same exact rectangle.