Come disegnare un pulsante personalizzato nella barra del titolo della finestra con Windows Form?
-
01-07-2019 - |
Domanda
Come si disegna un pulsante personalizzato accanto ai pulsanti Riduci a icona, Ingrandisci e Chiudi nella barra del titolo del modulo?
So che è necessario utilizzare le chiamate API Win32 e sovrascrivere la procedura WndProc, ma non sono stato in grado di trovare una soluzione che funzioni correttamente.
Qualcuno sa come farlo? Più specificamente, qualcuno conosce un modo per farlo che funziona in Vista?
Soluzione
Quanto segue funzionerà in XP, non ho macchine Vista a portata di mano per testarlo, ma penso che i tuoi problemi derivino da un hW e in qualche modo errato. Comunque, con il codice mal commentato.
// The state of our little button
ButtonState _buttState = ButtonState.Normal;
Rectangle _buttPosition = new Rectangle();
[DllImport("user32.dll")]
private static extern IntPtr GetWindowDC(IntPtr hWnd);
[DllImport("user32.dll")]
private static extern int GetWindowRect(IntPtr hWnd,
ref Rectangle lpRect);
[DllImport("user32.dll")]
private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
protected override void WndProc(ref Message m)
{
int x, y;
Rectangle windowRect = new Rectangle();
GetWindowRect(m.HWnd, ref windowRect);
switch (m.Msg)
{
// WM_NCPAINT
case 0x85:
// WM_PAINT
case 0x0A:
base.WndProc(ref m);
DrawButton(m.HWnd);
m.Result = IntPtr.Zero;
break;
// WM_ACTIVATE
case 0x86:
base.WndProc(ref m);
DrawButton(m.HWnd);
break;
// WM_NCMOUSEMOVE
case 0xA0:
// Extract the least significant 16 bits
x = ((int)m.LParam << 16) >> 16;
// Extract the most significant 16 bits
y = (int)m.LParam >> 16;
x -= windowRect.Left;
y -= windowRect.Top;
base.WndProc(ref m);
if (!_buttPosition.Contains(new Point(x, y)) &&
_buttState == ButtonState.Pushed)
{
_buttState = ButtonState.Normal;
DrawButton(m.HWnd);
}
break;
// WM_NCLBUTTONDOWN
case 0xA1:
// Extract the least significant 16 bits
x = ((int)m.LParam << 16) >> 16;
// Extract the most significant 16 bits
y = (int)m.LParam >> 16;
x -= windowRect.Left;
y -= windowRect.Top;
if (_buttPosition.Contains(new Point(x, y)))
{
_buttState = ButtonState.Pushed;
DrawButton(m.HWnd);
}
else
base.WndProc(ref m);
break;
// WM_NCLBUTTONUP
case 0xA2:
// Extract the least significant 16 bits
x = ((int)m.LParam << 16) >> 16;
// Extract the most significant 16 bits
y = (int)m.LParam >> 16;
x -= windowRect.Left;
y -= windowRect.Top;
if (_buttPosition.Contains(new Point(x, y)) &&
_buttState == ButtonState.Pushed)
{
_buttState = ButtonState.Normal;
// [[TODO]]: Fire a click event for your button
// however you want to do it.
DrawButton(m.HWnd);
}
else
base.WndProc(ref m);
break;
// WM_NCHITTEST
case 0x84:
// Extract the least significant 16 bits
x = ((int)m.LParam << 16) >> 16;
// Extract the most significant 16 bits
y = (int)m.LParam >> 16;
x -= windowRect.Left;
y -= windowRect.Top;
if (_buttPosition.Contains(new Point(x, y)))
m.Result = (IntPtr)18; // HTBORDER
else
base.WndProc(ref m);
break;
default:
base.WndProc(ref m);
break;
}
}
private void DrawButton(IntPtr hwnd)
{
IntPtr hDC = GetWindowDC(hwnd);
int x, y;
using (Graphics g = Graphics.FromHdc(hDC))
{
// Work out size and positioning
int CaptionHeight = Bounds.Height - ClientRectangle.Height;
Size ButtonSize = SystemInformation.CaptionButtonSize;
x = Bounds.Width - 4 * ButtonSize.Width;
y = (CaptionHeight - ButtonSize.Height) / 2;
_buttPosition.Location = new Point(x, y);
// Work out color
Brush color;
if (_buttState == ButtonState.Pushed)
color = Brushes.LightGreen;
else
color = Brushes.Red;
// Draw our "button"
g.FillRectangle(color, x, y, ButtonSize.Width, ButtonSize.Height);
}
ReleaseDC(hwnd, hDC);
}
private void Form1_Load(object sender, EventArgs e)
{
_buttPosition.Size = SystemInformation.CaptionButtonSize;
}
Altri suggerimenti
So che è passata molto tempo dall'ultima risposta, ma questo mi ha davvero aiutato di recente e mi piace aggiornare il codice fornito da Chris con i miei commenti e le mie modifiche. La versione funziona perfettamente su Win XP e Win 2003. Su Win 2008 ot ha un piccolo bug che non sono stato in grado di identificare durante il ridimensionamento di Windows. Funziona anche su Vista (no-Aero) ma nota che i pulsanti della barra del titolo non sono quadrati e le dimensioni dei pulsanti dovrebbero tenerne conto.
switch (m.Msg)
{
// WM_NCPAINT / WM_PAINT
case 0x85:
case 0x0A:
//Call base method
base.WndProc(ref m);
//we have 3 buttons in the corner of the window. So first's new button left coord is offseted by 4 widths
int crt = 4;
//navigate trough all titlebar buttons on the form
foreach (TitleBarImageButton crtBtn in titleBarButtons.Values)
{
//Calculate button coordinates
p.X = (Bounds.Width - crt * crtBtn.Size.Width);
p.Y = (Bounds.Height - ClientRectangle.Height - crtBtn.Size.Height) / 2;
//Initialize button and draw
crtBtn.Location = p;
crtBtn.ButtonState = ImageButtonState.NORMAL;
crtBtn.DrawButton(m.HWnd);
//increment button left coord location offset
crt++;
}
m.Result = IntPtr.Zero;
break;
// WM_ACTIVATE
case 0x86:
//Call base method
base.WndProc(ref m);
//Draw each button
foreach (TitleBarImageButton crtBtn in titleBarButtons.Values)
{
crtBtn.ButtonState = ImageButtonState.NORMAL;
crtBtn.DrawButton(m.HWnd);
}
break;
// WM_NCMOUSEMOVE
case 0xA0:
//Get current mouse position
p.X = ((int)m.LParam << 16) >> 16;// Extract the least significant 16 bits
p.Y = (int)m.LParam >> 16; // Extract the most significant 16 bits
p.X -= windowRect.Left;
p.Y -= windowRect.Top;
//Call base method
base.WndProc(ref m);
ImageButtonState newButtonState;
foreach (TitleBarImageButton crtBtn in titleBarButtons.Values)
{
if (crtBtn.HitTest(p))
{//mouse is over the current button
if (crtBtn.MouseButtonState == MouseButtonState.PRESSED)
//button is pressed - set pressed state
newButtonState = ImageButtonState.PRESSED;
else
//button not pressed - set hoover state
newButtonState = ImageButtonState.HOOVER;
}
else
{
//mouse not over the current button - set normal state
newButtonState = ImageButtonState.NORMAL;
}
//if button state not modified, do not repaint it.
if (newButtonState != crtBtn.ButtonState)
{
crtBtn.ButtonState = newButtonState;
crtBtn.DrawButton(m.HWnd);
}
}
break;
// WM_NCLBUTTONDOWN
case 0xA1:
//Get current mouse position
p.X = ((int)m.LParam << 16) >> 16;// Extract the least significant 16 bits
p.Y = (int)m.LParam >> 16; // Extract the most significant 16 bits
p.X -= windowRect.Left;
p.Y -= windowRect.Top;
//Call base method
base.WndProc(ref m);
foreach (TitleBarImageButton crtBtn in titleBarButtons.Values)
{
if (crtBtn.HitTest(p))
{
crtBtn.MouseButtonState = MouseButtonState.PRESSED;
crtBtn.ButtonState = ImageButtonState.PRESSED;
crtBtn.DrawButton(m.HWnd);
}
}
break;
// WM_NCLBUTTONUP
case 0xA2:
case 0x202:
//Get current mouse position
p.X = ((int)m.LParam << 16) >> 16;// Extract the least significant 16 bits
p.Y = (int)m.LParam >> 16; // Extract the most significant 16 bits
p.X -= windowRect.Left;
p.Y -= windowRect.Top;
//Call base method
base.WndProc(ref m);
foreach (TitleBarImageButton crtBtn in titleBarButtons.Values)
{
//if button is press
if (crtBtn.ButtonState == ImageButtonState.PRESSED)
{
//Rasie button's click event
crtBtn.OnClick(EventArgs.Empty);
if (crtBtn.HitTest(p))
crtBtn.ButtonState = ImageButtonState.HOOVER;
else
crtBtn.ButtonState = ImageButtonState.NORMAL;
}
crtBtn.MouseButtonState = MouseButtonState.NOTPESSED;
crtBtn.DrawButton(m.HWnd);
}
break;
// WM_NCHITTEST
case 0x84:
//Get current mouse position
p.X = ((int)m.LParam << 16) >> 16;// Extract the least significant 16 bits
p.Y = (int)m.LParam >> 16; // Extract the most significant 16 bits
p.X -= windowRect.Left;
p.Y -= windowRect.Top;
bool isAnyButtonHit = false;
foreach (TitleBarImageButton crtBtn in titleBarButtons.Values)
{
//if mouse is over the button, or mouse is pressed
//(do not process messages when mouse was pressed on a button)
if (crtBtn.HitTest(p) || crtBtn.MouseButtonState == MouseButtonState.PRESSED)
{
//return 18 (do not process further)
m.Result = (IntPtr)18;
//we have a hit
isAnyButtonHit = true;
//return
break;
}
else
{//mouse is not pressed and not over the button, redraw button if needed
if (crtBtn.ButtonState != ImageButtonState.NORMAL)
{
crtBtn.ButtonState = ImageButtonState.NORMAL;
crtBtn.DrawButton(m.HWnd);
}
}
}
//if we have a hit, do not process further
if (!isAnyButtonHit)
//Call base method
base.WndProc(ref m);
break;
default:
//Call base method
base.WndProc(ref m);
//Console.WriteLine(m.Msg + "(0x" + m.Msg.ToString("x") + ")");
break;
}
Il codice mostra i messaggi che devono essere trattati e come trattarli. Il codice utilizza una raccolta di oggetti personalizzati TitleBarButton. Quella classe è troppo grande per essere inclusa qui, ma posso fornirla se necessario insieme ad un esempio.
Il disegno sembra essere la parte facile, lo faranno i seguenti:
[Modifica: codice rimosso, vedi la mia altra risposta]
Il vero problema è cambiare lo stato e rilevare i clic sul pulsante ... per questo dovrai agganciarti al gestore di messaggi globale per il programma, .NET sembra nascondere gli eventi del mouse per un modulo mentre non sei in le aree effettive del contenitore (es. il mouse si sposta e fa clic sulla barra del titolo). Sto cercando informazioni su questo, l'ho trovato ora, ci sto lavorando, non dovrebbe essere troppo difficile ... Se riusciamo a capire cosa stanno passando questi messaggi.