Posts

Custom-drawn CheckBox and RadioButton

My take on a drop-in replacement for custom-drawn CheckBox and RadioButton controls:

internal class CheckBoxEx : CheckBox
{
private const float factor = 0.6f;
private const int check = 2;
public Color DisableFore = SystemColors.GrayText;
protected override void OnPaint( PaintEventArgs pevent )
{
base.OnPaint( pevent );
// To clear the background, it is necessary to use a non-transparent color and
// this type of controls usually are, so fetch the first parent's that is not
var backColor = BackColor;
while( backColor.A == 0 )
{
if( Parent == null )
{
break;
}
backColor = Parent.BackColor;
}
if( backColor.A == 0 )
{
// The back color is still transparent, so max the alpha component
backColor = Color.FromArgb( 255, backColor.R, backColor.G, backColor.B );
}
pevent.Graphics.Clear( backColor );
// Calculate the size of the box by using a large default character (since there could be no text
// in the control) and neither that ClientRectangle or pevent.ClipRectangle might have a valid value
var height = TextRenderer.MeasureText( pevent.Graphics, "X", Font ).Height;
var boxSide = (int)( height * factor );
var boxFrame = new Rectangle( 0, ( height - boxSide ) / 2, boxSide, boxSide );
var textFrame = new Rectangle( boxFrame.Right + 2, 0, ClientRectangle.Width - boxFrame.Right - 2, height );
if( Text.Length == 0 )
{
// The box cannot be aligned with non-existing text
boxFrame.Y = 0;
}
// Draw box
using var backBrush = new SolidBrush( BackColor );
using var foreBrush = new SolidBrush( ForeColor );
using var forePen = new Pen( ForeColor );
if( Enabled == false )
{
forePen.Color = foreBrush.Color = DisableFore;
}
pevent.Graphics.FillRectangle( backBrush, boxFrame );
pevent.Graphics.DrawRectangle( forePen, boxFrame );
if( Checked == true )
{
boxFrame.Offset( check, check );
boxFrame.Width -= check + 1;
boxFrame.Height -= check + 1;
pevent.Graphics.FillRectangle( foreBrush, boxFrame );
}
// Draw text
if( Text.Length != 0 )
{
using var sf = new StringFormat
{
Alignment = StringAlignment.Near,
LineAlignment = StringAlignment.Center
};
pevent.Graphics.SmoothingMode = SmoothingMode.HighQuality;
TextRenderer.DrawText( pevent.Graphics,
Text,
Font,
textFrame,
foreBrush.Color,
TextFormatFlags.Top | TextFormatFlags.Left );
pevent.Graphics.SmoothingMode = SmoothingMode.None;
if( Focused == true )
{
textFrame.Inflate( -1, 0 );
ControlPaint.DrawFocusRectangle( pevent.Graphics, textFrame, foreBrush.Color, backBrush.Color );
}
}
pevent.Dispose();
}
}
internal class RadioButtonEx : RadioButton
{
private const float factor = 0.6f;
private const int check = 3;
public Color DisableFore = SystemColors.GrayText;
protected override void OnPaint( PaintEventArgs pevent )
{
base.OnPaint( pevent );
// To clear the background, it is necessary to use a non-transparent color and
// this type of controls usually are, so fetch the first parent's that is not
var backColor = BackColor;
while( backColor.A == 0 )
{
if( Parent == null )
{
break;
}
backColor = Parent.BackColor;
}
if( backColor.A == 0 )
{
// The back color is still transparent, so max the alpha component
backColor = Color.FromArgb( 255, backColor.R, backColor.G, backColor.B );
}
pevent.Graphics.Clear( backColor );
// Calculate the size of the box by using a large default character (since there could be no text
// in the control) and neither that ClientRectangle or pevent.ClipRectangle might have a valid value
var height = TextRenderer.MeasureText( pevent.Graphics, "X", Font ).Height;
var boxSide = (int)( height * factor );
var boxFrame = new Rectangle( 0, ( height - boxSide ) / 2, boxSide, boxSide );
var textFrame = new Rectangle( boxFrame.Right + 2, 0, ClientRectangle.Width - boxFrame.Right - 2, height );
if( Text.Length == 0 )
{
// The box cannot be aligned with non-existing text
boxFrame.Y = 0;
}
// Draw box
pevent.Graphics.SmoothingMode = SmoothingMode.HighQuality;
using var backBrush = new SolidBrush( BackColor );
using var foreBrush = new SolidBrush( ForeColor );
using var forePen = new Pen( ForeColor );
if( Enabled == false )
{
forePen.Color = foreBrush.Color = DisableFore;
}
pevent.Graphics.FillEllipse( backBrush, boxFrame );
pevent.Graphics.DrawEllipse( forePen, boxFrame );
if( Checked == true )
{
boxFrame.Offset( check, check );
boxFrame.Width -= check * 2;
boxFrame.Height -= check * 2;
pevent.Graphics.FillEllipse( foreBrush, boxFrame );
}
// Draw text
if( Text.Length != 0 )
{
using var sf = new StringFormat
{
Alignment = StringAlignment.Near,
LineAlignment = StringAlignment.Center
};
TextRenderer.DrawText( pevent.Graphics,
Text,
Font,
textFrame,
foreBrush.Color,
TextFormatFlags.Top | TextFormatFlags.Left );
pevent.Graphics.SmoothingMode = SmoothingMode.None;
if( Focused == true )
{
textFrame.Inflate( -1, 0 );
ControlPaint.DrawFocusRectangle( pevent.Graphics, textFrame, foreBrush.Color, backBrush.Color );
}
}
pevent.Dispose();
}
}
view raw a.cs hosted with ❤ by GitHub

Event handlers with multiple arguments

From C# 7.0 on, named tuple types make it possible to simplify the declaration and use of event handlers with multiple parameters.

public class Example
{
public event EventHandler<string> PassString;
public event EventHandler<( Keys key, int x, bool modified )> PassMultipleValues;
private void NotifySubscribers()
{
PassString?.Invoke( this, "some text" );
PassMultipleValues?.Invoke( this, ( Keys.Up, 1, true ) );
}
}
...
public class Main
{
private Example _example = new();
public Main()
{
_example.PassString += OnPassString();
_example.PassMultipleValues += ( _, e ) => SomeOtherMethod( e.key, e.x, e.modified );
}
private void OnPassString( object sender, string text )
{
}
}
view raw a.cs hosted with ❤ by GitHub

SQLite-net, custom functions, and collections

It is not possible to define a custom function in SQLite-net that is able to use collections (sqlite does not work with managed code).

The code below presents an alternative that, although a hack, works.

using SQLitePCL;
public class Database
{
protected static SQLiteConnection _db;
public BaseDb()
{
if( _db == null )
{
_db = new SQLiteConnection( [DatabasePath] );
}
}
}
public class Phrase
{
public int Id{ get; set; }
public string Text{ get; set; }
}
public class TablePhrase : Database
{
private HashSet<int> _ids;
public TablePhrase()
{
SQLitePCL.raw.sqlite3_create_function( _db.Handle, "IN_SET", 3, null, InSet );
}
private void InSet( sqlite3_context ctx, object user_data, sqlite3_value[] args )
{
var id = raw.sqlite3_value_int( args[ 0 ] );
var text = raw.sqlite3_value_text( args[ 1 ] ).utf8_to_string();
var pattern = raw.sqlite3_value_text( args[ 2 ] ).utf8_to_string();
var result = 0;
if( _ids.Contains( id ) == true
&& Regex.IsMatch( text, pattern ) == true )
{
result = 1;
}
SQLitePCL.raw.sqlite3_result_int( ctx, result );
}
public List<Phrase> InSet( HashSet<int> ids, string pattern )
{
_ids = ids;
var query = $"SELECT * FROM Phrase WHERE IN_SET( id, text, {pattern} )";
var result = _db.Query<Phrase>( query );
_ids = null;
return result;
}
}
view raw a.cs hosted with ❤ by GitHub

ImageList fails to load the alpha channel

There is a bug in the ImageList component that ignores the alpha channel no matter the source format (.ico, .png) and distorts the display of images due to a failure to render transparency correctly. Thus the TabControl is so compromised in regards to icons in its tabs. The same applies to other controls that use ImageList.

This article provides a solution and explains the problem in more detail:

https://www.codeproject.com/articles/9142/adding-and-using-32-bit-alphablended-images-and-ic

To be able to use DrawImage in general, make sure to include the png as a resource and then load it as expected via var image = Properties.Resources.some_image

Otherwise, use ico files and DrawIcon

Increase the tab size in a user-drawn TabControl

By modifying the padding, it is possible to create additional space for multiple icons or other content.

https://docs.microsoft.com/en-us/windows/win32/controls/tab-controls#tab-size-and-position

https://docs.microsoft.com/en-us/windows/win32/controls/tcm-setpadding

https://docs.microsoft.com/en-us/windows/win32/api/_controls/

This is particularly useful in a TabControl with SizeMode = FillToRight because setting ControlStyles.UserPaint to true in the constructor will cause the system to report the size of tabs to match only the text (i.e., the space required for icons is unaccounted for).

My take:

private const int TCM_FIRST = 0x1300;
private const int TCM_SETPADDING = TCM_FIRST + 43;
private const int PADDING_X = 24;
private const int PADDING_Y = 2;
protected override void WndProc( ref Message m )
{
if( m.Msg == TCM_SETPADDING )
{
// The vertical padding is the high-order word and the horizontal the low-order word of the lParam
m.LParam = new IntPtr( ( PADDING_Y << 16 ) | ( PADDING_X & 0xFFFF ) );
}
base.WndProc( ref m );
}
view raw a.cs hosted with ❤ by GitHub

Remove horizontal scrollbar in a ListView

https://stackoverflow.com/a/2500089/1908746

My take:

public const int GWL_STYLE = -16;
public const int WS_HSCROLL = 0x00100000;
public const int WS_VSCROLL = 0x00200000;
[ DllImport( "user32.dll", EntryPoint = "GetWindowLong", CharSet = CharSet.Auto ) ]
public static extern IntPtr GetWindowLong32( IntPtr hWnd, int nIndex );
[ DllImport( "user32.dll", EntryPoint = "GetWindowLongPtr", CharSet = CharSet.Auto ) ]
public static extern IntPtr GetWindowLongPtr64( IntPtr hWnd, int nIndex );
[ DllImport( "user32.dll", EntryPoint = "SetWindowLong", CharSet = CharSet.Auto ) ]
public static extern IntPtr SetWindowLongPtr32( IntPtr hWnd, int nIndex, int dwNewLong );
[ DllImport( "user32.dll", EntryPoint = "SetWindowLongPtr", CharSet = CharSet.Auto ) ]
public static extern IntPtr SetWindowLongPtr64( IntPtr hWnd, int nIndex, int dwNewLong );
public static int GetWindowLong( IntPtr hWnd, int nIndex )
{
if( IntPtr.Size == 4 )
{
return (int)GetWindowLong32( hWnd, nIndex );
}
else
{
return (int)(long)GetWindowLongPtr64( hWnd, nIndex );
}
}
public static int SetWindowLong( IntPtr hWnd, int nIndex, int dwNewLong )
{
if( IntPtr.Size == 4 )
{
return (int)SetWindowLongPtr32( hWnd, nIndex, dwNewLong );
}
else
{
return (int)(long)SetWindowLongPtr64( hWnd, nIndex, dwNewLong );
}
}
protected override void WndProc( ref Message m )
{
switch( m.Msg )
{
case WM_NCCALCSIZE:
{
var style = (int)GetWindowLong( this.Handle, GWL_STYLE );
if( ( style & WS_HSCROLL ) == WS_HSCROLL )
{
SetWindowLong( Handle, GWL_STYLE, style & ~WindowsNative.WS_HSCROLL );
}
break;
}
}
base.WndProc( ref m );
}
view raw a.cs hosted with ❤ by GitHub

Remove the focus rectangle of a control

https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.control.showfocuscues?view=net-5.0

My take:

protected override bool ShowFocusCues
{
get
{
return false;
}
}
view raw a.cs hosted with ❤ by GitHub