Flickering Tab Control with MouseMove Determining What To Draw - c#

I have been researching this all day, (Go ahead and laugh lol) and I don't see any solutions to the age old Forms problem of flickering controls. My control is a TabControl and I am using DrawMode OwnerDrawFixed. I am hooking the following events. In short I am creating a TabControl with closable "X" buttons that are 12x12 png resources. The close buttons are all gray but if I mouse over one it should use a different image (a red X).
MouseDown: Loops all TabPages and checks if I have clicked on a rectangle where I am drawing my close button image.
MouseLeave: I need to Invalidate when I leave the TabControl to ensure everything is drawn correctly
MouseMove: Loops all TabPages and checks if I have moused over a rectangle where I am drawing my close button image. If I am mousing over then I save the tab page index so my paint can change the image used for the close button.
DrawItem: Here I simply draw the image
Things I have tested but no luck...
Making my own TabControl class which inherits TabControl and in the constructor I SetStyles for OptimizedDoubleBuffering To true (I set the other suggested flags to true)
I tried overriding CreateParams so I could or this value... createParams.ExStyle |= 0x00000020; (I have no idea what this does but read a user suggested to do this.
Setting the form DoubleBuffered (does nothing)
Anyways, I can't think what to do and I have read about this for awhile.
Here is my code for all events. I just want to have close buttons on my tabs that get highlighted when I mouse over them. Thanks.
private int mousedOver = -1;//indicates which close button is moused over
private void tabControl_DrawItem(object sender, DrawItemEventArgs e)
{
e.Graphics.DrawImage(e.Index == mousedOver ? Resources.redX : Resources.grayX, e.Bounds.Right - 15, e.Bounds.Top + 4);
}
private void tabControl_MouseDown(object sender, MouseEventArgs e)
{
TabControl tc = sender as TabControl;
if (tc.TabCount == 1) return;
for (int i = 0; i < tc.TabPages.Count; i++)
{
Rectangle r = tc.GetTabRect(i);
Rectangle closeButton = new Rectangle(r.Right - 15, r.Top + 4, 12, 12);
if (closeButton.Contains(e.Location))
{
TabPage tp = tc.TabPages[i];
tc.TabPages.Remove(tp);
tp.Dispose();
break;
}
}
}
private void tabControl_MouseMove(object sender, MouseEventArgs e)
{
TabControl tc = sender as TabControl;
for (int i = 0; i < tc.TabPages.Count; i++)
{
Rectangle r = tc.GetTabRect(i);
Rectangle closeButton = new Rectangle(r.Right - 15, r.Top + 4, 12, 12);
if (closeButton.Contains(e.Location))
{
mousedOver = i;
tc.Invalidate();
return;
}
}
mousedOver = -1;
tc.Invalidate();
}
private void tabControl_MouseLeave(object sender, EventArgs e)
{
TabControl tc = sender as TabControl;
mousedOver = -1;
tc.Invalidate();
}

It does look like you are invalidating too often. Try filtering it so that you only invalidate it when the control needs to be re-painted:
private void tabControl_MouseMove(object sender, MouseEventArgs e) {
TabControl tc = sender as TabControl;
for (int i = 0; i < tc.TabPages.Count; i++) {
Rectangle r = tc.GetTabRect(i);
Rectangle closeButton = new Rectangle(r.Right - 15, r.Top + 4, 12, 12);
if (closeButton.Contains(e.Location)) {
if (mousedOver != i) {
mousedOver = i;
tc.Invalidate(r);
}
} else if (mousedOver == i) {
int oldMouse = mousedOver;
mousedOver = -1;
tc.Invalidate(tc.GetTabRect(oldMouse));
}
}
}
I would keep the CreateParams override, but as a native windows control, you can probably never totally eliminate some flicker.

You could also try setting the DoubleBuffered property of the control, by doing
Control.DoubleBuffered = true;
I know that this works with DataGridViews, ListViews, Forms and Panels.
Documentation can be found on MSDN.

Related

Dynamically resize TabControl and Form width to the number of TabPages

I have a windows form with a TabControl and a ListView.
When I run the application, I want the Width of the TabControl to increase/decrease to show all the TabPages without horizontal scrollbar and have the Form resize it's Width accordingly, to insure that the TabControl and ListView are visible.
A screenshot is below.
To auto-size a TabControl to the size of its Headers, you need to calculate the width of the text of each Header. It's simpler in case the TabControl.SizeMode is set to Fixed, since you can set the ItemSize.Width and all Headers will have the same width.
If the TabControl.SizeMode is set to the default Normal, you have to measure the Text of each Header, adding 1px for the Border (2px if it's the second TabPage - small bug in the base Control).
In the first case, the size of the TabControl is:
tabControl1.Width = tabControl1.TabPages.Count * (tabControl1.ItemSize.Width + 1);
in the second case, measure the text of each Header using TextRendrer.MeasureText:
private int MeasureTabPagesWidth(TabControl tc)
{
if (tc.TabPages.Count == 0) return tc.Width;
int newWidth = 0;
int border = tc.TabPages.Count == 2 ? 2 : 1;
var flags = TextFormatFlags.LeftAndRightPadding;
using (var g = tc.CreateGraphics()) {
foreach (TabPage tab in tc.TabPages) {
newWidth += TextRenderer.MeasureText(g, tab.Text, tc.Font,
new Size(int.MaxValue, tc.Font.Height + 4), flags).Width + border;
}
}
return newWidth;
}
Setup the Layout:
Add a TableLayoutPanel to your Form, with one Row and two Columns (i.e., remove one Row)
Add the TabControl to the Cell on the left and the ListBox to the other Cell.
Set both Cells's style to AutoSize (after you have added your Controls).
Set the TableLayoutPanel to: AutoSize = true, AutoSizeMode = GrowAndShrink
Set the Form to auto-size in the same way
Set the Form's MinimumSize and MaximumSize. The former is usually set to the design size, the latter is up to you; you could use the current Screen WorkingArea as reference.
Calculate the new Width of the TabControl when the Form is created or loaded (i.e., in its Constructor or OnLoad() or Form.Load), so the Form will auto-size to the size of the TableLayoutPanel, whici in turn auto-sizes to the size of its child Controls.
Now you can add or remove TabPages at run-time and the Form will auto-size to the width you calculate in the TabControl.ControlAdded and TabControl.ControlRemoved event handlers (also checking whether the Control added is of Type TabPage).
Example:
The MeasureTabPagesWidth() method is the one shown above.
The TableLayoutPanel is named tlp1
The TabControl is named tabControl1
The Buttons used in the visual example have names that define their role.
public partial class AutoSizeForm : Form
{
public AutoSizeForm()
{
InitializeComponent();
tabControl1.Width = MeasureTabPagesWidth(tabControl1);
}
private void tabControl1_ControlAdded(object sender, ControlEventArgs e)
{
// Event notified after the TabPage has been added
if (e.Control is TabPage) {
tabControl1.Width = MeasureTabPagesWidth(tabControl1);
}
}
private void tabControl1_ControlRemoved(object sender, ControlEventArgs e)
{
if (e.Control is TabPage) {
// Use deferred execution, since the TabPage is removed after
// the event handler method completes.
BeginInvoke(new Action(()=> tabControl1.Width = MeasureTabPagesWidth(tabControl1)));
}
}
private void btnAddPage_Click(object sender, EventArgs e)
{
tabControl1.TabPages.Add(new TabPage("New TabpPage Text"));
}
private void btnRemovePage_Click(object sender, EventArgs e)
{
if (tabControl1.TabPages.Count > 0) {
tabControl1.TabPages.RemoveAt(tabControl1.TabPages.Count - 1);
}
}
private void btnAddCtlToTLP_Click(object sender, EventArgs e)
{
tlp1.ColumnCount += 1;
tlp1.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
var mc = new MonthCalendar();
tlp1.SetColumn(mc, tlp1.ColumnCount - 1);
tlp1.Controls.Add(mc);
}
}
This is how it works:
Tested in Windows 7, since this appears to be the System in use
Sample Project:
Sample Project on Google Drive (.Net Framework 4.8 - C# 7.3)
Rebuild the Solution before running
Starting out with that form, I'm going to add 8 tabs at run-time, calculate width of the text in the tabs + padding size x2 (both sides of the tabs) and then resize controls as needed.
public Form1()
{
InitializeComponent();
//Clear our default tabs.
tabControl1.TabPages.Clear();
//Add more tabs than would be visible by default
for (int i=1;i<=8;i++)
{
tabControl1.TabPages.Add("Tab " + i.ToString());
}
ResizeTabControl();
ResizeListViewControl();
ResizeForm();
}
void ResizeTabControl()
{
int tabCount = tabControl1.TabCount;
float length = 0;
using (Graphics g = CreateGraphics())
{
//Iterate through the tabs and get the length of the text.
for (int i = 0; i <= tabCount - 1; i++)
length += g.MeasureString(tabControl1.TabPages[i].Text, tabControl1.Font).Width;
}
//Resize the tab control where X is the length of all text in the tabs plus padding x 2 x total tabs.
tabControl1.Size = new Size(Convert.ToInt32(length) + (tabCount * 2 * tabControl1.Padding.X), tabControl1.Width);
}
void ResizeListViewControl()
{
//Move listview 10 pixels away from tabcontrol's edge
listView1.Location = new Point(tabControl1.Location.X + tabControl1.Width + 10, listView1.Location.Y);
}
void ResizeForm()
{
//Resize form to accomodate changes.
this.Width = listView1.Location.X + listView1.Width + 20;
}
After it's all said and done, this is what it looks like:
And with 20 tabs, because why not.

AutoScroll mode doesn't hide scrollbar properly

I have one Panel and when I click on add Button I add one Control under other added controls. When I click on another Button I remove Control which was added as last.
This works fine. On that panel I have AutoScroll setting set to True and when I add more controls it properly appears and I can use it. When I remove some controls Panel properly hides ScrollBar ONLY if the "animation" on that ScrollBar doesn't run at that time.
If no animation is running on that ScrollBar it disapears properly - doesn't matter if you have mouse over it or not.
If you have mouse over the ScrollBar and quickly move over the remove Button and click before ScrollBars animation is finished the Control is removed, but the inactive ScrollBar is still there. In Buttons click handler I tried to call Invalidate, Update and Refresh methods over the Panel but nothing works.
I tested this only on Windows 7.
If you don't know what I mean please try to look at this short video (20s without sound): http://youtu.be/-0EfRXrGbuc
You forgot to post mcve. So here is one (add panel and two buttons):
private void button1_Click(object sender, EventArgs e)
{
panel1.Controls.Add(new Button() { Top = panel1.Controls.Count * 30 });
}
private void button2_Click(object sender, EventArgs e)
{
if (panel1.Controls.Count > 0)
panel1.Controls.RemoveAt(panel1.Controls.Count - 1);
panel1.Refresh();
}
I am able to reproduce the problem
it's winforms, baby (c).
Possible workaround is to call Refresh() by using e.g. Timer or some of mouse events (it will not prevent issue, but use will easily fix it by e.g. moving mouse inside panel1) or you can postpone the possibility of deleting buttons itself for a short time after panel1.MouseLeave. All this kind of workarounds.
I hoped that there is some better way, but now I don't see any, so based on answer from Sinatr I decided to use Timer and cobined it with checking pixel Color to determine if the ScrollBar is still visible.
private Timer _timer = new Timer {Interval = 500};
public Form1()
{
InitializeComponent();
_timer.Tick += TimerOnTick;
}
private void button2_Click(object sender, EventArgs e)
{
if (panel1.Controls.Count > 0)
{
var wasVisible = panel1.VerticalScroll.Visible;
panel1.Controls.RemoveAt(panel1.Controls.Count - 1);
buttons.RemoveAt(buttons.Count - 1);
if (wasVisible != panel1.VerticalScroll.Visible)
{
_timer.Start();
}
}
}
private bool IsBackgroundColor()
{
var point = panel1.Location;
point.Offset(panel1.Width - 9, panel1.Height - 11);
point = PointToScreen(point);
Image imgScreen = new Bitmap(1, 1);
using (Bitmap bmp = new Bitmap(1, 1, PixelFormat.Format32bppArgb))
using (Graphics g = Graphics.FromImage(bmp))
using (Graphics gr = Graphics.FromImage(imgScreen))
{
g.CopyFromScreen(point, new Point(0, 0), new Size(1, 1));
gr.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
gr.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
gr.DrawImage(bmp, new Rectangle(0, 0, 1, 1));
var color = bmp.GetPixel(0, 0);
return color.R == panel1.BackColor.R && color.G == panel1.BackColor.G && color.B == panel1.BackColor.B;
}
}
private void TimerOnTick(object sender, EventArgs eventArgs)
{
if (!IsBackgroundColor() && !panel1.VerticalScroll.Visible)
{
panel1.Refresh();
}
else
{
_timer.Stop();
}
}
I was not able to use Panel.DrawToBitmap because it doesn't draw ScrollBars. I also start the Timer only when the ScrollBar was visible and now it shouldn't be.
It is important to mention that the pixel Color checking is possible only if you know the Color which should be there if the ScrollBar is hidden. It is not necessary have to be Panel.BackColor.

Superfluous scroll bars in WinForms

I have a WinForms application with a ListView which scales with the main program window. OnResize, I adjust the width of the columns to fit the full width of the ListView. Now when I switch from fullscreen to normal, I get a horizontal scrollbar. This scrollbar cannot be scrolled, i.e. the width it covers is 100% of the width of the ListView. In other words, I want to get rid of this scrollbar. If I shortly touch the window resize handlers, without actually resizing the window, the width is recomputed and the scrollbar disappears. How can I get rid of that scrollbar automatically?
column1.Width = fixedWidth;
column2.Width = listView.Width - fixedWidth - System.Windows.Forms.SystemInformation.VerticalScrollBarWidth;
There are two problems. It starts with you using the listview's Width property. Which isn't correct on a ListView with borders, you need to use the ClientSize.Width property. The second problem is related to way automatic layout is calculated, it causes the Resize event to fire too soon. You can work around that by delaying the adjustment with Control.BeginInvoke(). Like this:
private void listView1_Resize(object sender, EventArgs e) {
this.BeginInvoke(new Action(() => {
var lv = (ListView)sender;
var w = 0;
for (int ix = 0; ix < lv.Columns.Count - 1; ++ix) w += lv.Columns[ix].Width;
w = Math.Max(0, lv.ClientSize.Width - w);
lv.Columns[lv.Columns.Count - 1].Width = w;
}));
}
To fill the remaining space with the last column's width you can do this, of course we have to do this whenever the form is resized:
//Resize event handler for your Form1
private void Form1_Resize(object sender, EventArgs e){
column2.Width = -2;
}
//ColumnWidthChanged event handler
private void listView1_ColumnWidthChanged(object sender, ColumnWidthChangedEventArgs e){
int lastIndex = listView1.Columns.Count - 1;
if (e.ColumnIndex != lastIndex) listView1.Columns[lastIndex].Width = -2;
}
//ColumnWidthChanging event handler
private void listView1_ColumnWidthChanging(object sender, ColumnWidthChangingEventArgs e){
int lastIndex = listView1.Columns.Count - 1;
if (e.ColumnIndex != lastIndex) listView1.Columns[lastIndex].Width = -2;
}
NOTE: You said you had your ListView scaled with the main program window, I don't know what you did to achieve that, but I suggest you should use the Anchor property, set listView1.Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom;.

Switching Panels via Index Methods

I've been trying to solve my issue for quite a while and to be honest am getting nowhere. What i would like is when the user clicks the 'top' button on my panel it automatically goes to the top( and swaps with the one there.) and when they click the bottom button it automatically goes to the bottom. I'm setting the index panel manually but of course this doesnt work because its only viable for one panel (i have ten). Greatly appreciate some help in finding a method that can send the panel to the top of the stack regardless of its position.
Here is a image (basic) to help understand
Control ctrlToMove = (Control)this.bookControls[bookName];
int ctrlToMoveIndex = bookPanel.Controls.IndexOf(ctrlToMove);
int ctrlToSwapIndex = ctrlToMoveIndex - 5;
Control ctrlToSwap = bookPanel.Controls[ctrlToSwapIndex];
this.bookPanel.Controls.SetChildIndex(ctrlToMove, ctrlToSwapIndex);
this.bookPanel.Controls.SetChildIndex(ctrlToSwap, ctrlToMoveIndex);
Based on your drawing, I made a UserControl with a button on it:
void uc_ButtonClicked(object sender, EventArgs e) {
UserControl1 uc = sender as UserControl1;
if (uc != null) {
int childIndex = flowLayoutPanel1.Controls.GetChildIndex(uc);
if (childIndex > 0) {
UserControl1 ucTop = flowLayoutPanel1.Controls[0] as UserControl1;
flowLayoutPanel1.Controls.SetChildIndex(uc, 0);
flowLayoutPanel1.Controls.SetChildIndex(ucTop, childIndex);
}
}
}
According to your picture you have one control per row in panel. Thus I suggest you to use TableLayoutPanel instead of FlowLayoutPanel. Also I'd create user control for items in panel. E.g. it will have name PriorityUserControl and four buttons to increase, decrease, maximize, minimize it's 'priority' (I placed buttons horizontally just to save place on screen):
Next, create four events in this user control:
public event EventHandler PriorityMaximized;
public event EventHandler PriorityIncreased;
public event EventHandler PriorityDecreased;
public event EventHandler PriorityMinimized;
And rise appropriate event when button clicked:
private void topButton_Click(object sender, EventArgs e)
{
if (PriorityMaximized != null)
PriorityMaximized(this, EventArgs.Empty);
}
That's it. We have user control which tells whether it want to move up or down. Now add user controls to TableLayoutPanel (either manually or dynamically) and subscribe same event handlers of these four events to ALL user controls. Something like:
// create user control and attach event handlers
PriorityUserControl control = new PriorityUserControl();
control.PriorityMaximized += priorityUserControl_PriorityMaximized;
control.PriorityMinimized += priorityUserControl_PriorityMinimized;
control.PriorityIncreased += priorityUserControl_PriorityIncreased;
control.PriorityDecreased += priorityUserControl_PriorityDecreased;
// add another row to table
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowCount = panel.RowStyles.Count;
// add control table layout panel
panel.Controls.Add(control);
panel.SetRow(control, panel.RowCount - 1);
Good. All you should do now is implement these event handlers. It's simple. E.g. decreasing priority (i.e. moving down):
private void priorityUserControl_PriorityDecreased(object sender, EventArgs e)
{
// sender is a control where you clicked Down button
Control currentControl = (Control)sender;
// get position in panel
var position = panel.GetPositionFromControl(currentControl);
// just to be sure control is not one at the bottom
if (position.Row == panel.RowCount - 1)
return;
// we want to switch with control beneath current
Control controlToSwitch = panel.GetControlFromPosition(0, position.Row + 1);
// move both controls
panel.SetRow(currentControl, position.Row + 1);
panel.SetRow(controlToSwitch, position.Row);
}
Now implementation of maximizing priority (i.e. moving to top):
private void priorityUserControl_PriorityMaximized(object sender, EventArgs e)
{
Control currentControl = (Control)sender;
var position = panel.GetPositionFromControl(currentControl);
if (position.Row == 0 || panel.RowCount < 2)
return;
Control topControl = panel.GetControlFromPosition(0, 0);
panel.SetRow(currentControl, 0);
panel.SetRow(topControl, position.Row);
}
I believe you will create rest two handlers by yourself.
The key of what you want is setting up a clear and extendable algorithm capable to deal with the different positions of the Panels. Here you have a simple code showing certain approach to this problem:
public partial class Form1 : Form
{
int[] panelLocations;
Point[] pointLocations;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
panelLocations = new int[5];
pointLocations = new Point[5];
panelLocations[1] = 1;
panelLocations[2] = 2;
panelLocations[3] = 3;
pointLocations[1] = new Point(panel1.Left, panel1.Top);
pointLocations[2] = new Point(panel2.Left, panel2.Top);
pointLocations[3] = new Point(panel3.Left, panel3.Top);
}
private void relocate(int curPanel, bool goTop)
{
int curLoc = panelLocations[curPanel];
int newLoc = curLoc - 1;
if (!goTop)
{
newLoc = curLoc + 1;
}
if (newLoc < 1) newLoc = 3;
if (newLoc > 3) newLoc = 1;
if (newLoc != curLoc)
{
int otherIndex = Array.IndexOf(panelLocations, newLoc);
panelLocations[curPanel] = newLoc;
relocatePanel(curPanel);
panelLocations[otherIndex] = curLoc;
relocatePanel(otherIndex);
}
}
private void relocatePanel(int curIndex)
{
if (curIndex == 1)
{
panel1.Location = pointLocations[panelLocations[1]];
}
else if (curIndex == 2)
{
panel2.Location = pointLocations[panelLocations[2]];
}
else if (curIndex == 3)
{
panel3.Location = pointLocations[panelLocations[3]];
}
}
private void buttonTop1_Click(object sender, EventArgs e)
{
relocate(1, true);
}
private void buttonBottom1_Click(object sender, EventArgs e)
{
relocate(1, false);
}
}
Open a new project, add 3 panels (Panel1, Panel2 and Panel3... better put different background colors) and include two buttons (buttonUp and buttonDown). This code will make the Panel1 to go up and down (by changing its position with the other panels).
The idea is pretty simple: at the start you store the positions of all the Panels in an array. In another array, you store where each panel is located every time (1 is the original position of Panel1, etc.).
It is a quite simple code which you can improve and extend as much as required, but the idea is pretty reliable and you can use it in any case: a set of fixed positions through which the panels will be moving.

How to scroll in flowlayout panel without showing scrollbar in windows form

I am working on a touch screen POS in WinForms.
I have a flowlayoutpanel and add buttons dynamically but I dont want to show a scrollbar.
I use 2 buttons to scroll instead, so please help me how to scroll without showing a scrollbar
Try placing the FlowLayoutPanel inside another panel with these properties:
flowLayoutPanel1.AutoScroll = false;
flowLayoutPanel1.AutoSize = true;
flowLayoutPanel1.AutoSizeMode = AutoSizeMode.GrowAndShrink;
From here, you have to control yourself the location of the FlowLayoutPanel1 inside your panel (which should also have AutoScroll = false;) based on your two buttons.
Take two buttons btnLeft and btnRight and try this code :
private void btnLeft_Click(object sender, EventArgs e)
{
if (flowPanelItemCategory.Location.X <= xpos)
{
xmin = flowPanelItemCategory.HorizontalScroll.Minimum;
if (flowPanelItemCategory.Location.X >= xmin)
{
xpos -= 100;
flowPanelItemCategory.Location = new Point(xpos, 0);
}
}
}
private void btnRight_Click(object sender, EventArgs e)
{
if (flowPanelItemCategory.Location.X <= xpos)
{
xmax = flowPanelItemCategory.HorizontalScroll.Maximum;
if (flowPanelItemCategory.Location.X < xmax)
{
xpos += 100;
flowPanelItemCategory.Location = new Point(xpos, 0);
}
}
}

Categories