65.9K
CodeProject 正在变化。 阅读更多。
Home

Alpha 混合 Windows Forms

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.49/5 (34投票s)

2007年10月4日

CPOL

11分钟阅读

viewsIcon

224544

downloadIcon

11572

将 32 位图像设置为 Windows 窗体的背景。

AlphaForm Example

AlphaForm

引言

当人们希望将 32 位图像用作窗体的背景时,他们可能会发现 WS_EX_LAYERED 窗口样式,它允许您这样做。大多数人遇到的问题是,如果他们尝试简单地使用分层样式创建窗口,他们会发现自己能做的事情非常有限。例如,您无法将其设为子窗口,也无法在其上放置任何控件。

这个小库允许您使用相同的 32 位图像作为背景,而无需做出任何妥协,因此您可以做以前能够做的一切,此外还可以为自定义窗口和小部件提供更专业的 UI。

我最初写这篇文章是在这次更新前近三年,当时我知道它不是最好的解决方案,但它是少数可行方案之一。当时的设想是,现在仍然是,创建一个带有 WS_EX_LAYERED 样式的附加窗口,并使用该窗口来显示 alpha 混合的背景图像。然后我们只需确保这个窗口位于主窗体后面。

如何实现这样的功能

您可能会认为将两个窗口叠加起来是一个简单的任务,而我两次处理这个问题时都有同样的想法。然而,会出现一些问题,而这些问题的解决方案不可避免地会产生更多自己的问题。为了让您更好地理解它的工作原理,以及我为什么这样做,我将回顾我遇到的问题及其解决方案。您永远不知道,您可能会发现一些新东西!

定位窗口

如果我们要使用两个窗口来实现混合背景和顶部用户控件的效果,那么首先需要做的是将两个窗口定位在同一个位置。您的第一个想法可能是注册 LocationChanged 事件并将背景窗口设置为相同的位置。问题是一个窗口总是会稍微领先于另一个窗口,这可能会导致一些闪烁,这不会让您的辛勤工作看起来特别棒,并且可能会让一些用户望而却步。两个窗口之间的这种位置差异是我在图书馆的第一个版本中遇到的最大问题之一,我通过调用 DrawToBitmap 将所有窗口控件绘制到背景图像,隐藏主窗口,然后只移动背景来解决它。它的效果非常好,只是进行所有这些绘制需要大量时间,这导致窗口的移动有点跳跃。

Screenshot

图 1. 用户拖动时两个表单未对齐

这次我的解决方案分为两部分,第一部分是尝试确保两个窗口同时移动的更好方法,第二部分是再次将窗口绘制到背景表单。

为了使两个窗口作为一个整体(或尽可能接近)一起移动,您需要监听 WM_WINDOWPOSCHANGING 消息,该消息在窗体实际移动或调整大小之前发送。然后,我们可以利用此消息附带的信息使用 DeferWindowPos,这将阻止一个或多个窗口的绘制,直到整组窗口移动完毕。由于我们正在发送新消息来移动窗口,因此需要取消由初始 WM_WINDOWPOSCHANGING 消息引起的移动,这可以通过附加 SWP_NOMOVE 标志来实现。

WndProc(ref Message m)
{
  switch(m.Msg)
  {
    case WM_WINDOWPOSCHANGING:
      //Get the structure that holds all of the movement data
      Win32.WINDOWPOS posInfo = (Win32.WINDOWPOS)Marshal.PtrToStructure
				(m.LParam, typeof(Win32.WINDOWPOS));
		
      Win32.WindowPosFlags move_size = Win32.WindowPosFlags.SWP_NOMOVE | 
				Win32.WindowPosFlags.SWP_NOSIZE;
      if ((posInfo.flags & move_size) != move_size)
      {
        //Check for my own messages, which I do by setting to hwndInsertAfter to our 
        //own window, which from what I can gather only happens when you resize your
        //window, never when you move it
        if (posInfo.hwndInsertAfter != this.Handle)
        {
          IntPtr hwdp = Win32.BeginDeferWindowPos(2);
          if (hwdp != IntPtr.Zero)
            hwdp = Win32.DeferWindowPos(hwdp, m_layeredWnd.Handle, 
			this.Handle, posInfo.x, posInfo.y, 0, 0, 
                    	(uint)(posInfo.flags | Win32.WindowPosFlags.SWP_NOSIZE | 
			Win32.WindowPosFlags.SWP_NOZORDER));
          if (hwdp != IntPtr.Zero)
            hwdp = Win32.DeferWindowPos(hwdp, this.Handle, 
			this.Handle, posInfo.x, posInfo.y, posInfo.cx, 
                    posInfo.cy, (uint)
			(posInfo.flags | Win32.WindowPosFlags.SWP_NOZORDER));
          if (hwdp != IntPtr.Zero)
            Win32.EndDeferWindowPos(hwdp);

          //Update the flags so that the form will not move with this message
          posInfo.flags |= Win32.WindowPosFlags.SWP_NOMOVE;
          Marshal.StructureToPtr(posInfo, m.LParam, true);
        }
      }
      break;
  }
}

在大多数情况下,这应该足以提供两个窗口的平稳移动,但对于某些用户来说,这仍然不够,这就是为什么您可以选择在移动窗口时将整个窗口绘制到背景。为了使这种绘制尽可能快,我们不是为每个控件调用 DrawToBitmap,而是可以通过获取 Windows 用于将完全绘制的窗口显示在屏幕上的设备上下文来直接将窗口 BitBlt 到背景。要将窗口复制到背景,我们还需要创建一个蒙版,以便我们只复制窗口中不透明的区域。

创建蒙版很简单,我们所需要做的就是将窗口绘制到单色位图上,窗口上与背景颜色相同的任何像素都将绘制为白色,其他所有像素都将绘制为黑色。然后我们可以将此蒙版与 MaskBlt 函数一起使用,将窗口绘制到背景上。仅使用两次 BitBlt,整个操作只需 DrawToBitmap 所需时间的一小部分,这意味着除非您有特别大的背景图像或窗口,否则在开始移动窗口时应该没有可见的延迟。

IntPtr windowDC = Win32.GetWindowDC(this.Handle);	//Window DC that we are 
						//going to copy
IntPtr memDC = Win32.CreateCompatibleDC(windowDC);	//Temporary DC that we draw to
IntPtr BmpMask = Win32.CreateBitmap(this.ClientSize.Width, 
		this.ClientSize.Height, 1, 1, IntPtr.Zero);	//Mask bitmap
IntPtr BmpBack = backImage.GetHbitmap(Color.FromArgb(0));	//Background Image

//--Create mask
Win32.SelectObject(memDC, BmpMask);
//Set the colour that will become white
uint oldCol = Win32.SetBkColor(windowDC, 0X00FF00FF);
Win32.BitBlt(memDC, 0, 0, this.ClientSize.Width, 
	this.ClientSize.Height, windowDC, 0, 0, SRCCOPY);
Win32.SetBkColor(windowDC, oldCol);
//--

//Blit window to background image using mask
//We need to use the SPno raster operation with a white brush to combine our window
//with a black background before putting it onto the 32-bit background image, otherwise
//we end up with blending issues (source and destination colours are ANDed together)
Win32.SelectObject(memDC, BmpBack);
IntPtr brush = Win32.CreateSolidBrush(0x00FFFFFF);
Win32.SelectObject(memDC, brush);
Win32.MaskBlt(memDC, 0, 0, backImage.Width, 
	backImage.Height, windowDC, 0, 0, BmpMask, 0, 0, 0xCFAA0020);

获取鼠标输入

我们的主窗口将具有透明背景,因此它无法接收鼠标输入。除非您调用 Windows 函数 GetCapture(HWND hWnd),否则窗口不会收到透明背景区域的任何消息,但这会产生比解决更多的问题,因为它会导致桌面和其他窗口的常规 Windows 消息出现问题,因为您的应用程序正在捕获所有鼠标输入。那么您的解决方案可能是从背景窗口获取鼠标输入,该窗口应该捕获任何鼠标事件,因此您将改为注册背景窗口的鼠标事件。

当然,有问题。当您点击背景窗口时,它将成为活动窗口并绘制在您的控件上方。您可以尝试捕获此事件并恢复主窗口的焦点,但这仍然会导致闪烁,这非常令人不快。您可以应用一种窗口样式,该样式将阻止窗口获取焦点,这在某种程度上是有效的。背景窗口将不会获得焦点并被带到前面,但您的主窗口仍然会失去焦点。

我的解决方案是完全禁用背景窗口,这样对该窗口的任何操作都将被完全忽略,并且焦点将保留在我们的主窗口上。缺点是不会触发任何事件,因此我们需要查看窗口的消息。缺点再次是,禁用的窗口只接收一条消息 WM_SETCURSOR,Windows 使用它允许应用程序更改当前光标。幸运的是,此消息附带一些额外信息,即命中测试代码和鼠标的当前操作。鼠标操作以标准 Windows 消息 WM_MOUSEMOVEWM_LBUTTONDOWN 等形式给出。为了使用这些消息,我子类化了背景窗口,以便我可以拦截它的消息并检查 WM_SETCURSOR,然后触发适当的事件。

m_customLayeredWindowProc = new Win32.Win32WndProc(this.LayeredWindowWndProc);
//Set our new WndProc function and store the old one
m_layeredWindowProc = Win32.SetWindowLong
	(m_layeredWnd.Handle, GWL_WNDPROC, m_customLayeredWindowProc);

private int LayeredWindowWndProc(IntPtr hWnd, int Msg, int wParam, int lParam)
{
  //Check for events
  ...
  
  //Call the original WndProc function
  return Win32.CallWindowProc(m_layeredWindowProc, hWnd, Msg, wParam, lParam);
}

处理半透明窗口

由于我们有两个独立的窗口,如果透明度小于 1.0,我们的主窗口控件将与背景窗口混合,然后与桌面混合,而不是直接与桌面混合。这有一个相当简单的解决方案,因为我们知道窗口将一起移动,我们可以从背景图像中切出每个主窗口控件所在的区域,然后将这些区域绘制到主窗口的背景中。但需要注意的是,如果您将控件放置在背景图像中不完全不透明的区域上,则颜色将与窗口的背景颜色混合(在这种情况下,很可能是洋红色)。这是一个可选功能,可以根据您的要求启用。

图 2. 上图:未绘制控件背景的窗体。下图:绘制了控件背景的窗体。

请看上图中,您的控件如何与背景窗口混合,并注意到文本已绘制到 Windows 洋红色背景颜色上,使其呈现粉色轮廓。在下图中,我们将背景绘制在每个控件后面,因此文本绘制在实际背景图像上,消除了轮廓,并且按钮直接通过到桌面,因为我们已从背景图像本身中裁剪出该部分。

概述

因此,有了上述解决方案,最终设置看起来像这样

class AlphaForm : Form
{
  private LayeredWindow m_layeredWnd;
  private Bitmap m_backgroundImage;
	
  override OnLoad(EventArgs e)
  {
    base.OnLoad(e);
    
    UpdateLayeredWindow();
    m_layeredWnd.Show();

    //Subclass window to intercept messages
    ...
  }

  override OnPainBackground(...)
  {
    //If necessary draw a portion of the background image
    //behind each control
    foreach (Control ctrl in this.Controls)
    {
      Rectangle rect = ctrl.ClientRectangle;
      e.Graphics.DrawImage(m_backgroundImage, rect, rect, GraphicsUnit.Pixel);
    }
  }
	
  private int LayeredWindowWndProc(IntPtr hWnd, int Msg, int wParam, int lParam)
  {
    if(message is WM_SETCURSOR)
    {
      Point mousePos = System.Windows.Forms.Cursor.Position;
			
      MouseEvent = lParam >> 16;
      switch(MouseEvent)
      {
        case WM_MOUSEMOVE:
          MouseEventArgs e = new MouseEventArgs(...);
          this.OnMouseMove(e);
          break;
				
        etc.					
      }
    }
  }
	
  override WndProc(ref Message m)
  {
    switch(m.Msg)
    {
      case WM_WINDOWPOSCHANGING:
        //cancel movement by setting SWP_NOMOVE flag
			
        BeginDeferWindowPos();
        DeferWindowPos( main window );
        DeferWindowPos( background );
        EndDeferWindowPos();
        break;
        
      case WM_MOUSEMOVE:
        if(left mouse button is down)
        {
          //Copy window to background image
          Win32.MaskBlt(...);
          Win32.BitBlt(...);
          Win32.UpdateLayeredWindow(...);
          
          //Tell windows we are clicking the caption so that our window will
          //be dragged
          Win32.ReleaseCapture();
          Win32.SendMessage(this.Handle, 
	    (int)Win32.Message.WM_NCLBUTTONDOWN, (int)Win32.Message.HTCAPTION, 0);
        }
        break;
    }
  }
}

class LayeredWindow : Form
{
  void UpdateWindow(Bitmap image, byte opacity, int width = -1, int height = -1)
  {
    ...
    Win32.UpdateLayeredWindow(...);
  }
}

概括地说就是这样,还有更多的代码使其更具可用性,但这确实是您使它工作所需的一切。代码注释良好,因此如果您想详细了解所有内容的协同工作方式,花时间阅读它可能会很有价值。

Using the Code

在以前的版本中,您必须将主窗体传递给我的自定义 alpha 窗体,然后显示我的 alpha 窗体。这是一种奇怪的做法,因此在此版本中,我确保使其更简单。您可以通过输入 5 个字符来设置窗体以使用带 alpha 通道的背景图像。

//Your form with a regular window
public class Form1 : Form

//Your form with blended windows
public class Form1 : AlphaForm

继承自 AlphaForm 将允许您使用 32 位图像作为背景。您仍然需要负责将边框样式设置为无,并且所有内容都应与标准 Windows Form 完全相同,包括在设计器中编辑它(这是以前版本中缺少的功能)。一旦您选择了背景图像,它将在设计器中渲染到窗体上,以便您可以定位控件,但当您运行应用程序时,图像将仅显示在背景窗口上。

方法和属性

已添加了一些属性和方法,以便对背景窗体进行更多控制,我将在此处列出它们,并附上其用法的示例。

AlphaForm Class Diagram
Bitmap BlendedBackground 用作窗体背景的图像
bool EnhancedRendering 如果为 true,当窗体被拖动时,前景窗口将被绘制到背景窗口,然后隐藏。这可以防止两个窗体之间出现任何视觉差异。
bool RenderControlBackground 如果为 true,背景图像的一部分将绘制在窗体上的每个控件后面。
SizeModes SizeMode 更改窗体调整大小时的行为方式,可用选项如下
  • None:背景图像将始终保持其原始大小
  • Stretch:背景将调整大小以适应主窗体的客户区
  • Clip:背景图像将被裁剪到主窗体的客户区内
void DrawControlBackground(Control ctrl, bool drawBack) 当设置 RenderControlBackground 选项时,您可以使用此方法控制您的哪些控件将绘制背景。默认情况下,所有控件都设置为 true
void UpdateLayeredBackground() 当您添加、删除或移动了一些控件时,可用于强制更新窗体背景
void SetOpacity(double opacity) 应该用于设置窗体的透明度,而不是 Opacity 属性(否则您必须自己调用 UpdateLayeredBackground())。

每个属性的默认值都是 falseSizeMode 设置为 None

所有属性都可以在设计器中的 AlphaForm 类别下设置,但这里有一个设置具有所有功能的窗体的快速示例。请注意,由于在示例中 picBox 被排除在绘制背景之外,它将无法与窗体的其余部分正确混合,类似于图 2 中的顶部图像。

public partial class Form1 : AlphaForm
{
  public Form1()
  {
    InitializeComponent();
  }

  private void Form1_Load(object sender, EventArgs e)
  {
    this.BlendedBackground = new Bitmap(@"C:\myImage.png");
    this.SizeMode = SizeModes.Clip;
    this.DrawControlBackrounds = true;
    this.EnhancedRendering = true;
    DrawControlBackground(this.picBox, false);
    this.SetOpacity(0.75);
  }
}

结语

因此,通过这次更新,我希望能够解决人们可能遇到的一些奇怪问题,例如无法移动窗口或尝试从任务栏中移除窗口,并通过使其更易于使用来提高可用性。任何使用此功能的窗体都应与标准窗体表现完全相同,并且分层背景窗口与您自己的窗口之间的集成应该是无缝的。无论如何,这就是我所希望的。

历史

  • 16-09-2010
    • 演示和源代码更新,修复了几个错误
  • 12-09-2010
    • 文章重写
    • 代码更新到新版本
  • 3-12-2007
    • 更新了源代码和演示
    • 添加了 VB 启动代码
    • 添加了截图
    • 编辑了文章文本
  • 5-10-2007
    • 更新了 demo.zip 以包含源代码
    • 添加了有关设置的信息
  • 4-10-2007
    • 原始文章发布
© . All rights reserved.