Tabpage header stays below arrow buttons after moving - c#

The situation is as follows, I have a custom TabControl that paints the TabHeaders itself so I can choose colors etc.
The problem occurs when I have more TabHeaders then the TabControl can fit and the arrows (spin control) appears. Whenever I then cycle all the way to the right (so there are no more headers to the right) the tabheader that was behind the arrows is still partially there.
Image to demonstrate:
What I know:
If I manually call .Invalidate() so it repaints the problem will go away, however I can't find an event that is triggered by the arrow keys (it does not have a scroll event).
Events that won't work:
Click
Layout
Resize (don't even ask)
I had WndProc in mind (it has cases with get 'spin' which sounds like the spincontrol)
Custom tab control code:
class ColoredTabControl : TabControl
{
public Color ActiveTabColor
{
get
{
return _activeTabColor.Color;
}
set
{
_activeTabColor.Color = value;
this.Invalidate();
}
}
public Color DefaultTabColor
{
get
{
return _defaultTabColor.Color;
}
set
{
_defaultTabColor.Color = value;
this.Invalidate();
}
}
private SolidBrush _activeTabColor = new SolidBrush( Color.LightSteelBlue );
private SolidBrush _defaultTabColor = new SolidBrush( Color.LightGray );
private int _biggestTextHeight = 0;
private int _biggestTextWidth = 0;
public ColoredTabControl()
{
this.SetStyle( ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true );
}
protected override void OnPaint( PaintEventArgs e )
{
base.OnPaint( e );
var graphics = e.Graphics;
TabPage currentTab = this.SelectedTab;
if( TabPages.Count > 0 )
{
var biggestTextHeight = TabPages?.Cast<TabPage>()?.Max( r => TextRenderer.MeasureText( r.Text, r.Font ).Height );
var biggestTextWidth = TabPages?.Cast<TabPage>()?.Max( r => TextRenderer.MeasureText( r.Text, r.Font ).Width );
if( biggestTextHeight > _biggestTextHeight || biggestTextWidth > _biggestTextWidth )
{
_biggestTextWidth = ( int ) biggestTextWidth;
_biggestTextHeight = ( int ) biggestTextHeight;
this.ItemSize = new Size( _biggestTextWidth + 5, _biggestTextHeight + 10 );
}
}
for( int i = 0; i < TabPages.Count; i++ )
{
TabPage tabPage = TabPages[ i ];
Rectangle tabRectangle = GetTabRect( i );
SolidBrush brush = ( tabPage == currentTab ? _activeTabColor : _defaultTabColor );
graphics.FillRectangle( brush, tabRectangle );
TextRenderer.DrawText( graphics, tabPage.Text, tabPage.Font, tabRectangle, tabPage.ForeColor );
}
}
protected override void OnPaintBackground( PaintEventArgs e )
{
base.OnPaintBackground( e );
}
}
A bit of form code to repro the issue:
var tabControl = new ColoredTabControl() { Width = 340, Height = 300, Top = 100 };
tabControl.TabPages.Add( "text" );
tabControl.TabPages.Add( "another tab");
tabControl.TabPages.Add( "yet another" );
tabControl.TabPages.Add( "another one" );
tabControl.TabPages.Add( "another" );
tabControl.DefaultTabColor = Color.Red; //To see the issue clearly
Controls.Add( tabControl );

I ended up fixing it by hooking into WndProc event handler of the msctls_updown32 class. I did this by using a Win32 class I got from a codeproject tabcontrol project by Oscar Londono I had to adjust it a bit for my needs though.
Added code in the tabcontrol:
protected override void OnSelectedIndexChanged( EventArgs e )
{
base.OnSelectedIndexChanged( e );
this.Invalidate();
}
protected override void OnControlAdded( ControlEventArgs e )
{
base.OnControlAdded( e );
FindUpDown();
}
protected override void Dispose( bool disposing )
{
if(scUpDown != null)
{
scUpDown.SubClassedWndProc -= scUpDown_SubClassedWndProc;
}
base.Dispose( disposing );
}
bool bUpDown = false;
SubClass scUpDown = null;
private void FindUpDown()
{
bool bFound = false;
// find the UpDown control
IntPtr pWnd =
Win32.GetWindow( this.Handle, Win32.GW_CHILD );
while( pWnd != IntPtr.Zero )
{
// Get the window class name
char[] fullName = new char[ 33 ];
int length = Win32.GetClassName( pWnd, fullName, 32 );
string className = new string( fullName, 0, length );
if( className == Win32.MSCTLS_UPDOWN32 )
{
bFound = true;
if( !bUpDown )
{
// Subclass it
this.scUpDown = new SubClass( pWnd, true );
this.scUpDown.SubClassedWndProc +=
new SubClass.SubClassWndProcEventHandler(
scUpDown_SubClassedWndProc );
bUpDown = true;
}
break;
}
pWnd = Win32.GetWindow( pWnd, Win32.GW_HWNDNEXT );
}
if( ( !bFound ) && ( bUpDown ) )
bUpDown = false;
}
private int scUpDown_SubClassedWndProc( ref Message m )
{
switch(m.Msg)
{
case Win32.WM_LCLICK:
//Invalidate to repaint
this.Invalidate();
return 0;
}
return 0;
}
The other part of this class is still 100% the same as in the OP
Win32 class:
internal class Win32
{
/*
* GetWindow() Constants
*/
public const int GW_HWNDFIRST = 0;
public const int GW_HWNDLAST = 1;
public const int GW_HWNDNEXT = 2;
public const int GW_HWNDPREV = 3;
public const int GW_OWNER = 4;
public const int GW_CHILD = 5;
public const int WM_NCCALCSIZE = 0x83;
public const int WM_WINDOWPOSCHANGING = 0x46;
public const int WM_PAINT = 0xF;
public const int WM_CREATE = 0x1;
public const int WM_NCCREATE = 0x81;
public const int WM_NCPAINT = 0x85;
public const int WM_PRINT = 0x317;
public const int WM_DESTROY = 0x2;
public const int WM_SHOWWINDOW = 0x18;
public const int WM_SHARED_MENU = 0x1E2;
public const int WM_LCLICK = 0x201;
public const int HC_ACTION = 0;
public const int WH_CALLWNDPROC = 4;
public const int GWL_WNDPROC = -4;
//Class name constant
public const string MSCTLS_UPDOWN32 = "msctls_updown32";
[ DllImport("User32.dll",CharSet = CharSet.Auto)]
public static extern int GetClassName(IntPtr hwnd, char[] className, int maxCount);
[DllImport("User32.dll",CharSet = CharSet.Auto)]
public static extern IntPtr GetWindow(IntPtr hwnd, int uCmd);
}
#region SubClass Classing Handler Class
internal class SubClass : System.Windows.Forms.NativeWindow
{
public delegate int SubClassWndProcEventHandler(ref System.Windows.Forms.Message m);
public event SubClassWndProcEventHandler SubClassedWndProc;
private bool IsSubClassed = false;
public SubClass(IntPtr Handle, bool _SubClass)
{
base.AssignHandle(Handle);
this.IsSubClassed = _SubClass;
}
public bool SubClassed
{
get{ return this.IsSubClassed; }
set{ this.IsSubClassed = value; }
}
protected override void WndProc(ref Message m)
{
if (this.IsSubClassed)
{
if (OnSubClassedWndProc(ref m) != 0)
return;
}
base.WndProc(ref m);
}
private int OnSubClassedWndProc(ref Message m)
{
if (SubClassedWndProc != null)
{
return this.SubClassedWndProc(ref m);
}
return 0;
}
}
#endregion
If anyone has a better answer or a question feel free to answer/comment!

If anyone has a way or even thoughts on how to hook into the actual scroll event of the tabcontrol please answer/comment.
If you are curious to know how to get the scroll event of the tab header when you click on those scroll buttons, you can handle WM_HSCROLL event of the TabControl:
const int WM_HSCROLL = 0x0114;
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if (m.Msg == WM_HSCROLL)
this.Invalidate();
}
This way you can invalidate the control after scrolling using those buttons.
Also for the cases that user changed selected tab using keyboard, you can override OnSelectedIndexChanged and after calling base method, invalidate the control.

Related

Windows Forms: ctrl button prevents ListView from scrolling with mouse wheel

I want to scroll ListView with mouse wheel while Ctrl button is pressed. But apparently pressing Ctrl changes scroll behavior: it stops scrolling, possibly tries to apply some zooming logic, I don't know. And I can't find out how to override that.
Please any help or suggestions?
The solution to get mouse wheel scrolling working while Ctrl key is held down is to listen for the WndProc event and specifically detecting MOUSEWHEEL trigger, minimum simple working example:
ListBox with WndProc override
class CtrlListBoxScroll : ListBox
{
private const int WM_HSCROLL = 0x114;
private const int WM_VSCROLL = 0x115;
private const int WM_MOUSEWHEEL = 0x20A;
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if (m.Msg == WM_MOUSEWHEEL)
{
var scrollDirection = NativeMethods.GET_WHEEL_DELTA_WPARAM(m.WParam);
// scrolling down
if (this.TopIndex < this.Items.Count && scrollDirection < 0)
{
this.TopIndex += 1;
}
// scrolling up
if (this.TopIndex > 0 && scrollDirection > 0)
{
this.TopIndex -= 1;
}
}
}
}
NativeMethods to read the wParam and detect scroll direction
internal static class NativeMethods
{
internal static ushort HIWORD(IntPtr dwValue)
{
return (ushort)((((long)dwValue) >> 0x10) & 0xffff);
}
internal static ushort HIWORD(uint dwValue)
{
return (ushort)(dwValue >> 0x10);
}
internal static int GET_WHEEL_DELTA_WPARAM(IntPtr wParam)
{
return (short)HIWORD(wParam);
}
internal static int GET_WHEEL_DELTA_WPARAM(uint wParam)
{
return (short)HIWORD(wParam);
}
}
Then finally testing it
private void Form1_Load(object sender, EventArgs e)
{
var ctrlListBoxScroll = new CtrlListBoxScroll();
ctrlListBoxScroll.Items.AddRange
(
new object[]
{
"hello", "scroll", "bar", "pressing", "ctrl", "to scroll",
"this", "list", "box", "check", "ctrl", "key", "is", "held"
}
);
this.Controls.Add(ctrlListBoxScroll);
}

Tab height does not reflect high DPI on custom/userpaint TabControl

I have written a custom TabControl class, but I'm unable to make the tabs adapt their heights on high DPI screens. On a screen with 200% scaling, the tabs are halfway covered by the actual tab page and its controls, like this:
Apparently the TabControl doesn't adapt the tab heights to fit the larger font, and as a result, the top of the actual page is too high up and covers my tabs. What can I do to enforce the tabs to adapt?
The form has AutoScaleMode set to Dpi, and everything else looks fine, except for this. I'm targeting .NET 4.5.2, and the dpiAware setting is set to true in the manifest file.
Here is my code for the custom TabControl:
/// <summary>
/// A TabControl without 3D borders and other annoyances. Taken from
/// https://stackoverflow.com/questions/27469886/change-color-of-unused-space-of-tabcontrol/27472230
/// and modified.
/// </summary>
public class CleanTabControl : TabControl
{
private class NativeMethods
{
[DllImport("user32.dll")] public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
}
private const int WM_SETFONT = 0x30;
private const int WM_FONTCHANGE = 0x1d;
private int hoverTab = -1;
public event MouseEventHandler CloseClick;
public CleanTabControl()
{
// Take over the painting completely, we want transparency and double-buffering
SetStyle(ControlStyles.UserPaint | ControlStyles.SupportsTransparentBackColor, true);
DoubleBuffered = ResizeRedraw = true;
}
protected override void OnCreateControl()
{
// Necessary to give tabs the correct width
base.OnCreateControl();
OnFontChanged(EventArgs.Empty);
}
protected override void OnFontChanged(EventArgs e)
{
// Necessary to give tabs the correct width
base.OnFontChanged(e);
IntPtr hFont = Font.ToHfont();
NativeMethods.SendMessage(Handle, WM_SETFONT, hFont, (IntPtr)(-1));
NativeMethods.SendMessage(Handle, WM_FONTCHANGE, IntPtr.Zero, IntPtr.Zero);
UpdateStyles();
}
public override Color BackColor
{
// Override TabControl.BackColor, we need transparency
get { return Color.Transparent; }
set { base.BackColor = Color.Transparent; }
}
protected override void OnPaint(PaintEventArgs e)
{
// ... lot of painting code
}
protected override void OnMouseClick(MouseEventArgs e)
{
base.OnMouseClick(e);
if (SelectedTab != null)
{
if (GetImageRectangle(SelectedIndex).Contains(e.Location))
CloseClick(this, e);
}
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
hoverTab = -1;
for (int i = 0; i < TabCount; i++)
{
if (GetTabRect(i).Contains(e.Location))
{
if (GetImageRectangle(i).Contains(e.Location))
TabPages[i].ImageIndex = 1;
else
{
hoverTab = i;
TabPages[i].ImageIndex = 0;
}
}
else
TabPages[i].ImageIndex = 0;
}
Invalidate();
}
protected override void OnMouseLeave(EventArgs e)
{
base.OnMouseLeave(e);
hoverTab = -1;
for (int i = 0; i < TabCount; i++)
{
TabPages[i].ImageIndex = 0;
}
Invalidate();
}
private Rectangle GetImageRectangle(int index)
{
Rectangle r = GetTabRect(index);
int width = ImageList.ImageSize.Width;
int height = ImageList.ImageSize.Height;
int x = r.Right - width - Padding.X;
int y = (r.Top + r.Height - height) / 2 + 1;
if (index != SelectedIndex)
y += 1;
return new Rectangle(x, y, width, height);
}
}
}
I found the solution. In OnCreateControl(), add:
ItemSize = new Size(ItemSize.Width, ItemSize.Height * DpiRatio);
where DpiRatio is the scale factor (e.g. 2 for 200% scaling).

Setting value of a textfield to not go lower than zero

How do i set a textfield so it cant go lower than 0?
If you are using Windows Forms you can simply use the NumericUpDown, it also exposes Minimum and Maximum value fields.
If for some reason you will need to roll your own, you will probably need to attach yourself to the event which is fired when the text is changed. If the content of the text field is numeric and is lower than 0, then simply override the text with 0 or whatever value you wish to provide.
win form code :-
private void btnArrowUp_Click(object sender, EventArgs e)
{
int val = 0;
if (Convert.ToInt32(textBoxValue.Text) <= 1000) //convert the textBox value to integer and check for your Upper Limit (here i have set it to 1000)
{
// if so, increment by your step (i used 1)
val = Convert.ToInt32(textBoxValue.Text);
val += 1;
textBoxValue.Text = val.ToString();
labelErrorMessage.Visible = false;
}
else
{
// otherwise, give a message and reset to your Default (i used 0)
textBoxValue.Text = "0";
labelErrorMessage.Text = "1000 (Zero) is the Max limit !";
labelErrorMessage.Visible = true;
}
}
private void btnArrowDown_Click(object sender, EventArgs e)
{
int val = 0;
if (Convert.ToInt32(textBoxValue.Text) > 0) //convert the textBox value to integer and check for your Lower Limit (here i have set it to 0)
{
// if so, decrement by your step (i used 1)
val = Convert.ToInt32(textBoxValue.Text);
val -= 1;
textBoxValue.Text = val.ToString();
labelErrorMessage.Visible = false;
}
else
{
// otherwise, give a message and reset to your Default (i used 0)
textBoxValue.Text = "0";
labelErrorMessage.Text = "0 (Zero) is the lowest limit !";
labelErrorMessage.Visible = true;
}
}
Use NumericUpDown instead of text field and set Minimum = 0 or follow this
Add Balloon.cs
public enum TooltipIcon
{
None,
Info,
Warning,
Error
}
public class Balloon
{
private Control m_parent;
private string m_text = "FMS Balloon Tooltip Control Display Message";
private string m_title = "FMS Balloon Tooltip Message";
private TooltipIcon m_titleIcon = TooltipIcon.None;
private const int ECM_FIRST = 0x1500;
private const int EM_SHOWBALLOONTIP = ECM_FIRST + 3;
[DllImport("User32", SetLastError = true)]
private static extern int SendMessage(
IntPtr hWnd,
int Msg,
int wParam,
IntPtr lParam);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct EDITBALLOONTIP
{
public int cbStruct;
public string pszTitle;
public string pszText;
public int ttiIcon;
}
public Balloon()
{
}
public Balloon(Control parent)
{
m_parent = parent;
}
public void Show()
{
EDITBALLOONTIP ebt = new EDITBALLOONTIP();
ebt.cbStruct = Marshal.SizeOf(ebt);
ebt.pszText = m_text;
ebt.pszTitle = m_title;
ebt.ttiIcon = (int)m_titleIcon;
IntPtr ptrStruct = Marshal.AllocHGlobal(Marshal.SizeOf(ebt));
Marshal.StructureToPtr(ebt, ptrStruct, false);
System.Diagnostics.Debug.Assert(m_parent != null, "Parent control is null", "Set parent before calling Show");
int ret = SendMessage(m_parent.Handle, EM_SHOWBALLOONTIP, 0, ptrStruct);
Marshal.FreeHGlobal(ptrStruct);
}
public void Show(string text, Control parent, string title, TooltipIcon icon)
{
}
public string Title
{
get
{
return m_title;
}
set
{
m_title = value;
}
}
public TooltipIcon TitleIcon
{
get
{
return m_titleIcon;
}
set
{
m_titleIcon = value;
}
}
public string Text
{
get
{
return m_text;
}
set
{
m_text = value;
}
}
public Control Parent
{
get
{
return m_parent;
}
set
{
m_parent = value;
}
}
}
Add the following code to your Form
private readonly Balloon _balloonTip = new Balloon();
public Form1()
{
InitializeComponent();
}
private void textBox1_KeyPress(object sender, KeyPressEventArgs e)
{
e.Handled = HandledNumerics(sender, e);
}
private bool HandledNumerics(object sender, KeyPressEventArgs e)
{
var handled = !(Char.IsDigit(e.KeyChar) || (e.KeyChar == (char)Keys.Back) || (e.KeyChar == (char)Keys.Delete));
if (handled)
{
ShowBalloon("Warning", "Invalid Input!\nOnly numeric value is acceptable", (TextBox)sender);
}
return handled;
}
private void ShowBalloon(string title, string text, Control parent, TooltipIcon icon = TooltipIcon.Warning)
{
_balloonTip.Title = title;
_balloonTip.Text = text;
_balloonTip.Parent = parent;
_balloonTip.TitleIcon = icon;
_balloonTip.Show();
}
And get the result just like
simple way, just create a funcion that verifies is the value is bellow 0 and call it when the text changes, or in your down_click function if the text field is locked to keybord input.
private void bellowZero()
{
if (Convert.ToInt32(textBox1.Text) < 0)
{
textBox1.Text = "0";
}
}

How do I control two listboxes using a Vertical Scrollbar?

I've got a Windows Form that has 6 listboxes in it.
I'm trying to find / make a code that would make them scroll together. So I dropped a Vertical Scroll Bar onto the form, then put in the following code:
private void vScrollR_Scroll(object sender, ScrollEventArgs e)
{
int i = vScrollR.Value;
lstcr1.SelectedIndex = i;
lstpr1.SelectedIndex = i;
lstsr1.SelectedIndex = i;
lstcr2.SelectedIndex = i;
lstpr2.SelectedIndex = i;
lstsr2.SelectedIndex = i;
}
For some reason though, it won't work (i always returns 0). Am I going about this the wrong way? Is there any other way to achieve what I want? Perhaps, there's a method I need first?
Many thanks to all who will answer.
Change SelectedIndex to TopIndex. I just tried this and it works.
To keep the UI in sync while updating, you can use Control.BeginUpdate and Control.EndUpdate
listBox1.BeginUpdate();
listBox2.BeginUpdate();
listBox1.TopIndex =
listBox2.TopIndex = ++x;
listBox1.EndUpdate();
listBox2.EndUpdate();
Try to Create a Separate Class that Inherits from Listbox.
I hope that this will help you.
using System;
using System.Windows.Forms;
public class myScrollingListBox : ListBox
{
// Event declaration
public delegate void myScrollingListBoxDelegate(object Sender, myScrollingListBoxScrollArgs e);
public event myScrollingListBoxDelegate Scroll;
// WM_VSCROLL message constants
private const int WM_VSCROLL = 0x0115;
private const int SB_THUMBTRACK = 5;
private const int SB_ENDSCROLL = 8;
protected override void WndProc(ref Message m)
{
// Trap the WM_VSCROLL message to generate the Scroll event
base.WndProc(ref m);
if (m.Msg == WM_VSCROLL)
{
int nfy = m.WParam.ToInt32() & 0xFFFF;
if (Scroll != null && (nfy == SB_THUMBTRACK || nfy == SB_ENDSCROLL))
Scroll(this, new myScrollingListBoxScrollArgs(this.TopIndex, nfy == SB_THUMBTRACK));
}
}
public class myScrollingListBoxScrollArgs
{
// Scroll event argument
private int mTop;
private bool mTracking;
public myScrollingListBoxScrollArgs(int top, bool tracking)
{
mTop = top;
mTracking = tracking;
}
public int Top
{
get { return mTop; }
}
public bool Tracking
{
get { return mTracking; }
}
}
}

How to word wrap tooltips in C# [duplicate]

How to wordwrap text that need to be appear in ToolTip
It looks like it isn't supported directly:
How do I word-wrap the Tooltip that is displayed?
Here is a method using Reflection to
achieve this.
[ DllImport( "user32.dll" ) ]
private extern static int SendMessage( IntPtr hwnd, uint msg,
int wParam, int lParam);
object o = typeof( ToolTip ).InvokeMember( "Handle",
BindingFlags.NonPublic | BindingFlags.Instance |
BindingFlags.GetProperty,
null, myToolTip, null );
IntPtr hwnd = (IntPtr) o;
private const uint TTM_SETMAXTIPWIDTH = 0x418;
SendMessage( hwnd, TTM_SETMAXTIPWIDTH, 0, 300 );
Rhett Gong
Another way, is to create a regexp that wraps automatically.
WrappedMessage := RegExReplace(LongMessage,"(.{50}\s)","$1`n")
link
This is a piece I wrote recently, I know its not the best but it works. You need to extend the ToolTip Control as follows:
using System;
using System.Collections.Generic;
using System.Windows.Forms;
public class CToolTip : ToolTip
{
protected Int32 LengthWrap { get; private set; }
protected Control Parent { get; private set; }
public CToolTip(Control parent, int length)
: base()
{
this.Parent = parent;
this.LengthWrap = length;
}
public String finalText = "";
public void Text(string text)
{
var tText = text.Split(' ');
string rText = "";
for (int i = 0; i < tText.Length; i++)
{
if (rText.Length < LengthWrap)
{
rText += tText[i] + " ";
}
else
{
finalText += rText + "\n";
rText = tText[i] + " ";
}
if (tText.Length == i+1)
{
finalText += rText;
}
}
}
base.SetToolTip(Parent, finalText);
}
}
And you will use it like:
CToolTip info = new CToolTip(Control,LengthWrap);
info.Text("It looks like it isn't supported directly. There is a workaround at
http://windowsclient.net/blogs/faqs/archive/2006/05/26/how-do-i-word-wrap-the-
tooltip-that- is-displayed.aspx:");
You can set the size of the tooltip using e.ToolTipSize property, this will force word wrapping:
public class CustomToolTip : ToolTip
{
public CustomToolTip () : base()
{
this.Popup += new PopupEventHandler(this.OnPopup);
}
private void OnPopup(object sender, PopupEventArgs e)
{
// Set custom size of the tooltip
e.ToolTipSize = new Size(200, 100);
}
}
For WPF, you can use the TextWrapping property:
<ToolTip>
<TextBlock Width="200" TextWrapping="Wrap" Text="Some text" />
</ToolTip>

Categories