Overriding DrawItem for ListBox - unselected items are not redrawn - c#

This is a C# desktop application. The DrawStyle property of my ListBox is set to OwnerDrawFixed.
The problem: I override DrawItem to draw text in different fonts, and it works. But when I start resizing the form at the runtime, the selected item is drawn correctly, but the rest of them are not redrawn, causing text looking corrupt for unselected items.
Here's my code:
private void listDevices_DrawItem(object sender, DrawItemEventArgs e)
{
e.DrawBackground();
string textDevice = ((ListBox)sender).Items[e.Index].ToString();
e.Graphics.DrawString(textDevice,
new Font("Ariel", 15, FontStyle.Bold), new SolidBrush(Color.Black),
e.Bounds, StringFormat.GenericDefault);
// Figure out where to draw IP
StringFormat copy = new StringFormat(
StringFormatFlags.NoWrap |
StringFormatFlags.MeasureTrailingSpaces
);
copy.SetMeasurableCharacterRanges(new CharacterRange[] {new CharacterRange(0, textDevice.Length)});
Region[] regions = e.Graphics.MeasureCharacterRanges(
textDevice, new Font("Ariel", 15, FontStyle.Bold), e.Bounds, copy);
int width = (int)(regions[0].GetBounds(e.Graphics).Width);
Rectangle rect = e.Bounds;
rect.X += width;
rect.Width -= width;
// draw IP
e.Graphics.DrawString(" 255.255.255.255",
new Font("Courier New", 10), new SolidBrush(Color.DarkBlue),
rect, copy);
e.DrawFocusRectangle();
}
listDevices.Items.Add("Device001");
listDevices.Items.Add("Device002");
Also, the item that is drawn correctly (the selected one) is flickering on form resizing. No biggie, but if anyone know why.... tnx

Put the following code in the Resize event:
private void listDevices_Resize(object sender, EventArgs e) {
listDevices.Invalidate();
}
This should cause everything to be redrawn.
To stop the flickering, you need double buffering.
To do this, make a new class, derived from ListBox, and put the following in the constructor:
this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
Or just paste this into a code file:
using System.Windows.Forms;
namespace Whatever {
public class DBListBox : ListBox {
public DBListBox(): base() {
this.DoubleBuffered = true;
// OR
// this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
}
}
}
Replace "Whatever" with the namespace your project uses, or make it something more useful. AFter compiling, you should be able to add a DBListBox in the form designer.

I repro the problem. There are several mistakes in the code, the font name is "Arial", you should not adjust rect.Width, you forget to call Dispose() on the fonts, brushes and regions. But they don't explain the behavior. There's something wrong with the clipping area that prevents the text from being properly updated. I don't see where that occurs, the Graphics object state is okay.
Graphics.DrawString() is a very troubled method, you should really avoid it. All Windows Forms controls, including ListBox, use TextRenderer.DrawText(). That solves the problem when I use it. I know measuring is more difficult, you could work around that by displaying the IP address at a fixed offset. Looks better too, they'll line up in a column that way.
It flickers because you use e.DrawBackground(). That erases the existing text, you draw the text right back on it. I don't think double-buffering is going to fix that, you'd have to draw the entire item so you don't have to draw the background. Tricky if you can't get the exact size of the text with the large font, a workaround is to draw into a bitmap first.

Related

How to create transparent controls in Windows Forms when controls are layered

I am trying to implement a "Fillable Form" in which editable text fields appear over top of an image of a pre-preprinted form for a dot matrix printer. (using c# and Windows Forms and targeting .Net 2.0) My first idea was to use the image as the Windows Form background, but it looked horrible when scrolling and also did not scroll properly with the content.
My next attempt was to create a fixed-size window with a panel that overflows the bounds of the window (for scrolling purposes.) I added a PictureBox to the panel, and added my textboxes on top of it. This works fine, except that TextBoxes do not support transparency, so I tried several methods to make the TextBoxes transparent. One approach was to use an odd background color and a transparency key. Another, described in the following links, was to create a derived class that allows transparency:
Transparency for windows forms textbox
TextBox with a Transparent Background
Neither method works, because as I have come to find out, "transparency" in Windows Forms just means that the background of the window is painted onto the control background. Since the PictureBox is positioned between the Window background and the TextBox, it gives the appearance that the TextBox is not transparent, but simply has a background color equal to the background color of the Window. With the transparency key approach, the entire application becomes transparent so that you can see Visual Studio in the background, which is not what I want. So now I am trying to implement a class that derives from TextBox and overrides either OnPaint or OnPaintBackground to paint the appropriate part of the PictureBox image onto the control background to give the illusion of transparency as described in the following link:
How to create a transparent control which works when on top of other controls?
First of all, I can't get it working (I have tried various things, and either get a completely black control, or just a standard label background), and second of all, I get intermittent ArgumentExceptions from the DrawToBitmap method that have the cryptic message "Additional information: targetBounds." Based on the following link from MSDN, I believe that this is because the bitmap is too large - in either event it seems inefficient to capture the whole form image here because I really just want a tiny piece of it.
https://msdn.microsoft.com/en-us/library/system.windows.forms.control.drawtobitmap(v=vs.100).aspx
Here is my latest attempt. Can somebody please help me with the OnPaintBackground implementation or suggest a different approach? Thanks in advance!
public partial class TransparentTextbox : TextBox
{
public TransparentTextbox()
{
InitializeComponent();
SetStyle(ControlStyles.OptimizedDoubleBuffer |
ControlStyles.AllPaintingInWmPaint |
ControlStyles.ResizeRedraw |
ControlStyles.UserPaint, true);
}
protected override void OnPaintBackground(PaintEventArgs e)
{
//base.OnPaintBackground(e); // not sure whether I need this
if (Parent != null)
{
foreach (Control c in Parent.Controls)
{
if (c.GetType() == typeof(PictureBox))
{
PictureBox formImg = (PictureBox)c;
Bitmap bitmap = new Bitmap(formImg.Width, formImg.Height);
formImg.DrawToBitmap(bitmap, formImg.Bounds);
e.Graphics.DrawImage(bitmap, -Left, -Top);
break;
}
}
Debug.WriteLine(Name + " didn't find the PictureBox.");
}
}
}
NOTE: This has been tagged as a duplicate, but I referenced the "duplicate question" in my original post, and explained why it was not working. That solution only works if the TextBox sits directly over the Window - if another control (such as my Panel and PictureBox) sit between the window and the TextBox, then .Net draws the Window background onto the TextBox background, effectively making its background look gray, not transparent.
I think I have finally gotten to the bottom of this. I added a Bitmap variable to my class, and when I instantiate the textboxes, I am setting it to contain just the portion of the form image that sits behind the control. Then I overload OnPaintBackground to display the Bitmap, and I overload OnPaint to manually draw the text string. Here is the updated version of my TransparentTextbox class:
public partial class TransparentTextbox : TextBox
{
public Bitmap BgBitmap { get; set; }
public TransparentTextbox()
{
InitializeComponent();
SetStyle(ControlStyles.OptimizedDoubleBuffer |
ControlStyles.AllPaintingInWmPaint |
ControlStyles.ResizeRedraw |
ControlStyles.UserPaint, true);
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.DrawString(this.Text, this.Font, Brushes.Black, new PointF(0.0F, 0.0F));
}
protected override void OnPaintBackground(PaintEventArgs e)
{
e.Graphics.DrawImage(BgBitmap, 0, 0);
}
}
... and here is the relevant part of how I instantiate:
Bitmap bgImage = (Bitmap)Bitmap.FromStream(Document.FormImage);
PictureBox pb = new PictureBox();
pb.Image = bgImage;
pb.Size = pb.Image.Size;
pb.Top = 0;
pb.Left = 0;
panel1.Controls.Add(pb);
foreach (FormField field in Document.FormFields)
{
TransparentTextbox tb = new TransparentTextbox();
tb.Width = (int)Math.Ceiling(field.MaxLineWidth * 96.0);
tb.Height = 22;
tb.Font = new Font("Courier", 12);
tb.BorderStyle = BorderStyle.None;
tb.Text = "Super Neat!";
tb.TextChanged += tb_TextChanged;
tb.Left = (int)Math.Ceiling(field.XValue * 96.0);
tb.Top = (int)Math.Ceiling(field.YValue * 96.0);
tb.Visible = true;
Bitmap b = new Bitmap(tb.Width, tb.Height);
using (Graphics g = Graphics.FromImage(b))
{
g.DrawImage(bgImage, new Rectangle(0, 0, b.Width, b.Height), tb.Bounds, GraphicsUnit.Pixel);
tb.BgBitmap = b;
}
panel1.Controls.Add(tb);
}
I still need to work on how the text looks when I highlight it, and other things like that, but I feel like I am on the right track. +1 to Reza Aghaei and Mangist for commenting with other viable solutions!

Tooltip text formatting : *Making partly bold*

Issue: I want to make Tooltip text Partly bold.
Example text that i want to show on tooltip:
label1: label1Value
label2: label2Value
Progress so far: I have made a custom tooptip class with my own implementation of Draw event. I have used:
DrawToopTipEventArgs newAgrs = new DrawToopTipEventArgs (
e.Graphics, e.AssociatedWindow, e.AssociatedControl,
e.Bounds, e.ToolTipText, this.BackColor, this.ForeColor,
new Font("Arial Unicode MS", 8.25F, FontStyle.Bold));
newAgrs.DrawText(TextFormatFlags.TextBoxControl);
Main Issue: This way am able to make complete tooltiptext bold, but still stuck how i can make only the value part bolds and keep rest of the labels as it is. (as shown above).
You'll have to measure your text and draw everything yourself.
private void ToolTip_Draw(object sender, DrawToolTipEventArgs e)
{
using (var boldFont = new Font(e.Font, FontStyle.Bold))
{
var headerText = "Header: ";
var valueText = "Value";
var headerTextSize = TextRenderer.MeasureText(headerText, e.Font);
TextRenderer.DrawText(e.Graphics, headerText, e.Font, e.Bounds.Location, Color.Black);
var valueTextPosition = new Point(e.Bounds.X + headerTextSize.Width, e.Bounds.Y);
TextRenderer.DrawText(e.Graphics, valueText, boldFont, valueTextPosition, Color.Black);
}
}
I hard-coded the header and value strings for simplicity. It should be trivial to extend this to work for multiple lines. The measured text size has a height, you can also get the height from the Font object itself.
The reason you're example is making everything bold is that you're just delegating all the drawing to a instance of DrawToolTipEventArgs with a new font and telling it to draw the tool tip text (all of it) with the new arguments. It happily took the new font and draw all the text using it.
One additional thing to keep in mind is that the Popup event should also be handled. That even is used to measure the size of the tooltip so you have enough room to draw your text. If you don't handle it, it may not be wide enough to handle the bold font. Specifically, you'll want to set the PopupEventArgs.ToolTipSize property.

Custom CheckBox in C#

I want to have a custom CheckBox in C# that has a gradiant backgronud on it. I overrided OnPaint(PaintEventArgs e) as below:
Graphics g = e.Graphics;
base.OnPaint(e);
//// Fill the background
//SetControlSizes();
// Paint the outer rounded rectangle
g.SmoothingMode = SmoothingMode.AntiAlias;
using (GraphicsPath outerPath = GeneralUtilities.RoundedRectangle(mLabelRect, 1, 0))
{
using (LinearGradientBrush outerBrush = new LinearGradientBrush(mLabelRect,
mGradientTop, mGradientBottom, LinearGradientMode.Vertical))
{
g.FillPath(outerBrush, outerPath);
}
using (Pen outlinePen = new Pen(mGradientTop, mRectOutlineWidth))
{
outlinePen.Alignment = PenAlignment.Inset;
g.DrawPath(outlinePen, outerPath);
}
}
//// Paint the gel highlight
using (GraphicsPath innerPath = GeneralUtilities.RoundedRectangle(mHighlightRect, mRectCornerRadius, mHighlightRectOffset))
{
using (LinearGradientBrush innerBrush = new LinearGradientBrush(mHighlightRect,
Color.FromArgb(mHighlightAlphaTop, Color.White),
Color.FromArgb(mHighlightAlphaBottom, Color.White), LinearGradientMode.Vertical))
{
g.FillPath(innerBrush, innerPath);
}
}
// Paint the text
TextRenderer.DrawText(g, Text, Font, mLabelRect, Color.White, Color.Transparent,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis);
It works and make a gradiant for background, but checkbox disappear under the gradiant and can't access. Now, what shall I do ??? please help me as soon as possible
EDIT:
OK I know what's wrong. The checkbox draws an underlaying background automatically that covers anything previously drawn.
In this case, you must draw the appearance of the checkbox (i.e. the checked state, etc.) by yourself.
You should override the OnPaintBackground function for drawing the background, instead of OnPaint.
Another option is to call base.OnPaint(e) after you've drawn the background.
The checkbox doesn't "disappear" under the gradient and you can still access it. You just drawn the "background" above the "foreground".
The base control draw the appearance of the checkbox in the base.OnPaint(e) function. If you draw anything after calling it, those things will be drawn as an "overlay" in front of the drawn checkbox, that's why you can't see the appearance of the checkbox.
If you are going to draw the Text by yourself also, you would not want the internally-drawn checkbox text to appear. In this case you will need to draw the appearance of the checkbox by yourself also.
As I already mentioned, if you are only going to draw a custom background, use OnPaintBackground instead.

Custom control onPaint event not working

Hey people I have a problem I am writing a custom control. My control inherits from Windows.Forms.Control and I am trying to override the OnPaint method. The problem is kind of weird because it works only if I include one control in my form if I add another control then the second one does not get draw, however the OnPaint method gets called for all the controls. So what I want is that all my custom controls get draw not only one here is my code:
If you run the code you will see that only one red rectangle appears in the screen.
public partial class Form1 : Form
{
myControl one = new myControl(0, 0);
myControl two = new myControl(100, 0);
public Form1()
{
InitializeComponent();
Controls.Add(one);
Controls.Add(two);
}
private void Form1_Load(object sender, EventArgs e)
{
}
}
public class myControl:Control
{
public myControl(int x, int y)
{
Location = new Point(x, y);
Size = new Size(100, 20);
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Pen myPen = new Pen(Color.Red);
e.Graphics.DrawRectangle(myPen, new Rectangle(Location, new Size(Size.Width - 1, Size.Height - 1)));
}
}
I'm guessing you are looking for something like this:
e.Graphics.DrawRectangle(Pens.Red, new Rectangle(0, 0,
this.ClientSize.Width - 1,
this.ClientSize.Height - 1));
Your Graphic object is for the interior of your control, so using Location isn't really effective here. The coordinate system starts at 0,0 from the upper-left corner of the client area of the control.
Also, you can just use the built-in Pens for colors, otherwise, if you are creating your own "new" pen, be sure to dispose of them.
LarsTech beat me to it, but you should understand why:
All drawing inside of a control is made to a "canvas" (properly called a Device Context in Windows) who coordinates are self-relative. The upper-left corner is always 0, 0.
The Width and Height are found in ClientSize or ClientRectangle. This is because a window (a control is a window in Windows), has two areas: Client area and non-client area. For your borderless/titlebar-less control those areas are one and the same, but for future-proofing you always want to paint in the client area (unless the rare occasion occurs where you want to paint non-client bits that the OS normally paints for you).

Draw a line on a TabPage in C#

I am having trouble drawing a line on a TabPage.
I actually have a TabControl inside a TabControl. I have drawn a number of labels which I am using as boxes. I want to draw lines to join them together.
I have tried:
Pen P = new Pen(System.Drawing.Color.Black, 10);
tabname.CreateGraphics().DrawLine(P, 10, 10, 100, 100);
and
Pen P = new Pen(System.Drawing.Color.Black, 10);
tabcontrolname.TabPages[0].CreateGraphics().DrawLine(P, 10, 10, 100, 100);
Neither are displaying the line. I assume the line is being placed somewhere as there are no errors.
Any ideas how I can get it to display on the correct TabPage?
Thank you!
You probably need to override the OnPaint method (or handle the Paint event) to get this to work properly. If you don't your controls will just end up drawing over your lines.
Here's a link to the relevant docs.
Where do you try these codes, in which function? If you are doing it once in the initialization or construction, they will not be displayed as you expect. Whenever the control needs to be redrawn, you need to draw this line too, again. Either override the OnPaint method of the control or register for the Paint event and do the line drawing there.
I was able to get the arrow to show up using the following code:
TabPage.Paint += new PaintEventHandler(TabPage_Paint);
and
protected void TabPage_Paint(object sender, PaintEventArgs e)
{
base.OnPaint(e);
Pen arrow = new Pen(Brushes.Black, 4);
arrow.EndCap = System.Drawing.Drawing2D.LineCap.ArrowAnchor;
e.Graphics.DrawLine(arrow, 10, 10, 100, 100);
arrow.Dispose();
}
However, when scrolling is initiated the Paint messes up :(

Categories