OpenGL 和 WPF: 在 WPF 窗口中集成 OpenGL 窗口





5.00/5 (1投票)
演示了如何在 .NET WPF 应用程序中使用 OpenGL,通过将 OpenGL 窗口直接作为主 WPF 窗口的子窗口来集成,使其行为更像一个控件。
引言
本文使用 OpenTK 作为 .NET 库来访问 .NET 上的 OpenGL。该库还提供了 GLWpfControl
,这是一个用于与 WPF 应用集成的控件。可能还有其他库也提供 WPF 集成的选项,但描述使用 OpenTK 与其他库相比的理由,或者使用 WPF、OpenGL 等的理由,不属于本文的范围——这些都相当公理化:“给定 WPF、OpenGL 和 OpenTK,那么……”
唯一需要提及的是,不幸的是,GLWpfControl
的性能并未达到我的预期,至少在我的实际应用中是这样(见文章结尾)。事实上,它的性能甚至不如仅仅使用 WPF 自定义动画进行逐帧渲染。因此,挑战在于直接在 WPF 中使用 OpenTK 的 GameWindow
并使其表现得像一个控件。为此,以下是该窗口的一系列要求或验收标准:
- 窗口必须没有边框,没有标题栏。
- 窗口必须固定在其主窗口上,也就是说,它不能位于主窗口后面,也不能有其他窗口插在其之间。
- 窗口必须“粘”在主窗口内指定的区域,并且在移动或调整主窗口大小时必须跟随主窗口。
- 窗口必须被限制(裁剪)在主窗口的边界内——它不能超出这些边界(例如,移动/缩放)。
- 窗口不得捕获键盘焦点。
- 窗口对鼠标输入必须透明,也就是说,鼠标输入必须穿透到下方的 WPF 内容。
- 可选:窗口背景必须是透明的。
- 窗口必须允许应用程序控制其定位、大小调整和渲染。
现在我们列出此解决方案与使用 GLWpfControl
等专用控件相比的优缺点。
优点
- 由于与 WPF 的互操作性,没有性能下降。尽管我不是这方面的专家,但根据我的理解,所有这些互操作 WPF 控件(以及 SharpGL 的
OpenGLControl
或 MonoGame 的MonogameContentControl
)都有一个共同点:它们使用 DirectX 互操作,并通过 WPF 的D3DImage
类将绘制结果复制到可用缓冲区中。有额外的缓冲区复制和刷新操作,肯定会对性能产生一定影响……而直接使用GameWindow
则不会出现这种情况。 - OpenTK 的
GameWindow
不需要任何 OpenGL 扩展,只需要核心实现。与之相反,GLWpfControl
需要NV_DX_interop
扩展,而讽刺的是,我的笔记本电脑的 NVIDIA GPU 不支持此扩展,但集成的 Intel 显卡却支持!所以,如果你没有此扩展,那就无能为力了。
缺点
- 虽然控件集成在 WPF 布局中,带有边距、水平/垂直对齐等,但
GameWindow
不是该布局的一部分,因此控件所理所当然的定位必须通过代码手动管理。但并非全无好处,正如我们稍后将看到的。 - 你无法在
GameWindow
上方渲染 WPF 内容,例如叠加层。任何与窗口占据相同空间的渲染都将位于其下方。我不认为这种用例很常见,我反而认为透明窗口背景是一种更常见的用例。也就是说,窗口**就是**叠加层。本文将讨论透明度问题。
从现在开始,我们将重点讨论压缩的源代码(顶部链接)中包含的应用程序。该应用程序的基本外观和行为在顶部的动画图片中有所展示。
代码剖析
应用程序代码结构
源代码是 Visual Studio 2022 解决方案的形式,其中包含一个面向 .NET 6 的 WPF 应用程序项目。OpenTK
库作为 NuGet 包 从 nuget.org 安装,版本设置为 **4.8.2**,这是撰写本文时最新的稳定版本。
代码基于 OpenTK 教程中的“Hello Triangle”示例,该教程托管在 GitHub 上。与本文相关的类是 MainWindow
和 GLWindow
。我将 GLWindow
的实现拆分为两个源文件。并非我赞同这种做法,但这样做是为了帮助大家专注于本文的重要内容。文件 GLWindow.article.cs 包含本文相关的部分——OpenGL 窗口的创建和管理逻辑。文件 GLWindow.tutorial.cs 以及着色器文件涵盖了 OpenGL 特定的代码,我不会过多强调,因为这些内容直接来自上述 OpenTK 教程,并且在那里得到了最好的阐述。还有一个小的 static
类 Interop
,它充当一些必需的 Win32 函数的 PInvoke 包装器。
GLWindow 构造函数
GLWindow
是 OpenTK
的 GameWindow
的子类,负责 OpenGL 渲染。在 引言中,我们列出了此窗口必须满足的基本要求,所以让我们看看代码是如何实现的。我们从构造函数开始。
private GLWindow(IntPtr hWndParent, Vector2i location, Vector2i size)
: base(GameWindowSettings.Default,
new NativeWindowSettings {
Location = location,
ClientSize = size,
WindowBorder = WindowBorder.Hidden
})
{
unsafe {
GLFW.HideWindow(WindowPtr);
IntPtr ptr = GLFW.GetWin32Window(WindowPtr);
uint childStyle = Interop.GetWindowLong(ptr, Interop.GWL_STYLE);
childStyle |= Interop.WS_CHILD;
childStyle &= ~Interop.WS_POPUP;
_ = Interop.SetWindowLong(ptr, Interop.GWL_STYLE, childStyle);
_ = Interop.SetWindowLong(ptr, Interop.GWL_EXSTYLE,
Interop.WS_EX_TOOLWINDOW | Interop.WS_EX_LAYERED | Interop.WS_EX_TRANSPARENT);
_ = Interop.SetParent(ptr, hWndParent);
_ = Interop.EnableWindow(ptr, false);
_ = Interop.SetLayeredWindowAttributes(ptr, 0x00000000, 0, Interop.LWA_COLORKEY);
GLFW.ShowWindow(WindowPtr);
}
_xpos = location.X;
_ypos = location.Y;
}
构造函数调用基类版本,通过 NativeWindowSettings
传入一些设置:窗口的初始位置和大小,这些设置从主窗口获取;以及一个非常有用的 WindowBorder.Hidden
设置,用于创建一个没有边框和标题栏的窗口(这是要求之一)。我们没有为要使用的 OpenGL 版本指定任何设置,而是让它使用默认的 **3.3** 版本。
当构造函数体被进入时,窗口已经由基类构造函数创建。OpenTK 为我们提供了对窗口 WindowPtr
的底层不安全指针和底层 GLFW API(OpenGL 的 glfw3.dll 的包装器),我们可以使用它们。我们使用 GLFW.HideWindow
在修改窗口之前隐藏它,并使用 GLFW.GetWin32Window
来检索窗口的 Win32 HANDLE
,我们稍后需要它。因此,在构造函数体中,我们开始进行修改以满足我们的要求,这些要求要求将窗口设置为主窗口的**子**窗口。OpenTK 没有提供任何选项,所以我们需要通过 PInvoke 包装器 Interop
使用 Win32 API 函数 SetParent。根据 MSDN 的说法,我们需要在调用此函数之前清除 WS_POPUP
样式并设置 WS_CHILD
样式。我们通过调用 GetWindowLong 获取当前样式,然后调用 SetWindowLong 来相应地修改样式。我们需要通过一些**扩展样式**标志来进一步修改样式,以满足更多要求。我们通过第二次调用 SetWindowLong
并结合一些标志来实现这一点。
WS_EX_TOOLWINDOW
防止其出现在任务栏中。WS_EX_LAYERED
|WS_EX_TRANSPARENT
与SetLayeredWindowAttributes
结合使用以启用透明度。
接下来的调用如下:
- EnableWindow,它禁用对窗口的鼠标和键盘输入。鼠标事件将穿透到下方的 WPF 窗口,键盘事件也将被定向到父 WPF 窗口。
- SetLayeredWindowAttributes 设置窗口的透明度。在此情况下,通过
0x00bbggrr
值传递的颜色键 (LWA_COLORKEY
) 将黑色 (0x00000000
) 设置为透明颜色。这与 OpenGL 渲染中使用的背景色相匹配,意味着窗口背景是透明的。所有其他渲染的颜色将是不透明的,这正是我们想要的。
最后,我们通过调用 GLFW.ShowWindow
显示窗口,并记录窗口的初始位置,以便在必要时更新它。
构造函数到此结束;关于 SetLayeredWindowAttributes
以及 OpenTK 对透明背景、窗口透明度和鼠标穿透的支持,还有更多内容要评论,但我将在文章末尾的**结论**部分进行评论。
GLWindow 线程
此时,我们有一个子窗口,它被限制在其父窗口内,无边框,禁用鼠标和键盘输入,背景透明。我们已经非常接近满足引言中的要求了。
GLWindow
有一个从其父类 GameWindow
继承的渲染循环——Run
方法——它会使调用线程忙碌。这意味着该线程不能与主线程(即 WPF UI 线程)相同。因此,窗口在 Task
中创建和运行,该 Task
从线程池获取一个后台线程。要做到这一点,我们需要关闭 OpenTK 中的一个标志(属性):GLFWProvider.CheckForMainThread = false
,否则在创建窗口时会引发异常。我们在一个 static
方法中执行此操作,并创建和运行任务。
public static GLWindow? CreateAndRun(IntPtr hWndParent, Rect bounds)
{
GLFWProvider.CheckForMainThread = false;
using var mres = new ManualResetEventSlim(false);
GLWindow? glWnd = null;
_GLTask = Task.Run(() => {
glWnd = new GLWindow(hWndParent, ((int)bounds.X, (int)bounds.Y),
((int)bounds.Width, (int)bounds.Height));
mres.Set();
using (glWnd) {
glWnd?.Run();
}
});
mres.Wait();
return glWnd;
}
CreateAndRun
用于从主线程调用,并通过调用其 Task.Run
方法来启动存储在 _GLTask
static
成员中的任务。主线程传递父窗口的 Win32 HANDLE
(包装在 IntPtr
中),这是 SetParent
所必需的,以及子窗口的初始边界。任务通过其构造函数简单地创建子窗口,然后启动其渲染循环,该循环是继承的 Run
方法。从那时起,渲染循环将持续运行,直到应用程序结束,子窗口关闭。
主线程和任务之间存在一些同步:主线程在并行任务完成子窗口创建之前不会返回。这是通过 ManualResetEventSlim
实现的,主线程在启动任务后等待它被信号,而任务在创建子窗口后发出信号。
与 GLWindow 通信
为了完成要求,我们需要能够控制子窗口的重新定位和大小调整。另外,还可以控制其隐藏和显示。控制渲染内容和渲染时机也很常见。此应用程序遵循 OpenTK 教程,其中所有需要渲染的内容(所有顶点缓冲区)都在窗口加载时(OnLoad
)确定,但更实际的做法应该是更流畅、由应用程序控制。
这意味着我们需要与子窗口通信——设置其位置和大小,调用显示和隐藏方法,设置影响渲染循环中渲染的参数。由于这是一个在不同线程中创建并运行其渲染循环的窗口,因此线程安全性变得至关重要。根据 OpenTK 的 FAQ 部分,尽管 OpenTK 是线程安全的,但 OpenGL 本身并非如此。很难从中判断哪些具体是线程安全的,哪些不是。移动和隐藏窗口可能没问题,因为它们是操作系统级别的操作,不涉及 OpenGL。但是,为了安全起见,并且确保万无一失,我们遵循一个通用规则:**我们对窗口及其渲染采取的任何操作都必须是线程安全的(即使是移动和隐藏)**。
我们采用的模式是一种松散的命令模式,即“命令”通过线程安全的 ConcurrentQueue
对象从主线程发送到子窗口的线程。GLWindow
提供了一系列 public
方法来处理这些命令,主线程可以直接调用这些方法,但这些方法仅将命令(命令是 Action
)加入队列。以下是这些方法:
public void Cleanup()
{
EnqueueCommand(() => {
Close();
});
_GLTask?.Wait();
}
public void SetBoundingBox(Rect bounds)
{
EnqueueCommand(() => {
ClientLocation = ((int)bounds.X, (int)bounds.Y);
ClientSize = ((int)bounds.Width, (int)bounds.Height);
_xpos = bounds.X; _ypos = bounds.Y;
});
}
public void MoveBy(int deltaX, int deltaY)
{
EnqueueCommand(() => {
_xpos += deltaX; _ypos += deltaY;
ClientLocation = ((int)_xpos, (int)_ypos);
});
}
public void Show()
{
EnqueueCommand(() => {
unsafe {
GLFW.ShowWindow(WindowPtr);
}
});
}
public void Hide()
{
EnqueueCommand(() => {
unsafe {
GLFW.HideWindow(WindowPtr);
}
});
}
public void ToggleSpin()
{
EnqueueCommand(() => {
_isSpinStopped = !_isSpinStopped;
});
}
private void EnqueueCommand(Action command) =>
_commands.Enqueue(command);
我不会过多强调这些方法,因为它们应该是不言自明的。仅提及 Cleanup
是主 WPF 窗口关闭时调用的方法。调用 Close
会结束渲染循环,因此任务也会结束。主线程等待任务结束,确保子窗口关闭(以及与之相关的清理)完成。清理代码在 GLWindow.tutorial.cs 中重写的 OnUnload
方法中。在我们的例子中,这是可选的,因为当应用程序退出时,进程所持有的资源(如顶点缓冲区)会由操作系统释放;然而,拥有一个有纪律的关闭过程并执行特定的清理也是不错的。
同样,ToggleSpin
是控制渲染的一个简单示例。虽然在这里它只是停止或重新启动三角形的旋转,但在实际应用中,这可以扩展为更高级的功能,例如控制要渲染的对象。
GLWindow
是 ConcurrentQueue
的消费者,该队列存储在成员 _commands
中。但问题是,在哪里可以合适地消费这个队列?OpenTK 的教程表明,重写的 OnUpdateFrame
是处理键盘和鼠标输入的地方。在我们的例子中,窗口不处理任何键盘或鼠标输入,因此我们用处理“命令输入”来替换该处理。由于 OnUpdateFrame
与 OnRenderFrame
一样由渲染循环 Run
调用,因此队列中的任何 Action
都会在子窗口的线程上执行,这正是我们想要的。以下是相关方法:
protected override void OnUpdateFrame(FrameEventArgs args)
{
base.OnUpdateFrame(args);
while (TryDequeueCommand(out Action? command)) {
command();
}
}
private bool TryDequeueCommand([MaybeNullWhen(returnValue: false)] out Action command) =>
_commands.TryDequeue(out command);
至此,子窗口满足了引言中的所有要求。接下来回顾主 WPF 窗口,它揭示了 GLWindow
的使用方式。
主窗口
<Window x:Class="WpfOpenTK.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfOpenTK"
mc:Ignorable="d"
Title="OpenTK in WPF" Height="600" Width="650"
Background="#000020"
Loaded="Window_Loaded"
Closing="Window_Closing"
KeyDown="Window_KeyDown"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Rectangle Name="RenderArea" Grid.Row="1"
Grid.Column="1" Stroke="#008800"
SizeChanged="RenderArea_SizeChanged"/>
<StackPanel Grid.Row="1" Grid.Column="2"
HorizontalAlignment="Left" VerticalAlignment="Center">
<ToggleButton Width="100" Checked="HideShowButton_Checked"
Unchecked="HideShowButton_Unchecked">
<ToggleButton.Style>
<Style TargetType="ToggleButton">
<Setter Property="Content" Value="Hide"/>
<Style.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Content" Value="Show"/>
</Trigger>
</Style.Triggers>
</Style>
</ToggleButton.Style>
</ToggleButton>
<ToggleButton Width="100" Margin="0,5,0,0" Click="StopStartButton_Click">
<ToggleButton.Style>
<Style TargetType="ToggleButton">
<Setter Property="Content" Value="Start spin"/>
<Style.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Content" Value="Stop spin"/>
</Trigger>
</Style.Triggers>
</Style>
</ToggleButton.Style>
</ToggleButton>
</StackPanel>
</Grid>
</Window>
主 WPF 窗口由一个 3x3 的网格分隔;中间的单元格是为 OpenGL 渲染指定的区域。在该单元格中,我们放置了一个 Rectangle
,它除了定义 GLWindow
的边界框(OpenGL 渲染发生在此处)外,没有其他作用。这个边界框是相对于主窗口的。随着主窗口的调整大小和矩形的位置及大小发生变化,GLWindow
也会随之变化。这种布局选择完全是任意的,实际应用将有自己的布局,渲染区域由业务需求决定。
主窗口中的其他元素是两个按钮,用于向 GLWindow
发送一些命令——一个用于隐藏和显示窗口,另一个用于停止和恢复渲染三角形的旋转。主窗口还定义了加载、关闭和一些键盘输入的事件处理程序。
在回顾代码后台时,我们将依次讨论以下功能:
- 创建和关闭子窗口
- 调整主窗口及其子窗口的大小
- 通过箭头键移动子窗口来平移三角形
- 使用按钮隐藏/显示和控制三角形的旋转
创建和关闭子窗口
private void Window_Loaded(object sender, RoutedEventArgs e)
{
IntPtr hWndParent = new WindowInteropHelper(this).Handle;
_glWnd = GLWindow.CreateAndRun(hWndParent, GetRenderAreaBounds());
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
_glWnd?.Cleanup();
}
在父窗口的 Loaded
事件处理程序中创建子窗口。在这里,我们获取父窗口的 Win32 HANDLE
并调用前面讨论的 static
方法 CreateAndRun
。GetRenderAreaBounds
方法将在下一段中讨论。在父窗口的 Closing
事件处理程序中关闭子窗口,它只需调用子窗口的 Cleanup
方法。请记住,此方法会将关闭子窗口的命令加入队列,然后等待子窗口的线程确认它即将结束。
调整主窗口及其子窗口的大小
当主窗口调整大小时,网格单元格也会调整大小,因此我们需要跟踪中间单元格中矩形的位置,以便更新子窗口的位置和大小。为此,我们处理矩形的 SizeChanged
事件。
private void RenderArea_SizeChanged(object sender, SizeChangedEventArgs e)
{
_glWnd?.SetBoundingBox(GetRenderAreaBounds());
}
private Rect GetRenderAreaBounds()
{
Point location = RenderArea.TransformToAncestor(this).Transform(new Point(0, 0));
return new Rect {
X = location.X,
Y = location.Y,
Width = RenderArea.ActualWidth,
Height = RenderArea.ActualHeight
};
}
重要方法是 GetRenderAreaBounds
,它重新获取矩形的位置(左上角)相对于主窗口(this
)。该方法还在创建子窗口时用于获取初始位置。对于大小调整,事件处理程序调用我们之前已经见过的子窗口的 SetBoundingBox
,它会将命令加入队列,最终更新子窗口的 ClientLocation
和 ClientSize
属性。
通过箭头键移动子窗口来平移三角形
主窗口的 KeyDown
事件处理程序捕获箭头键的按下,并在相应方向上移动子窗口。如果三角形到达主窗口的边界,它将被裁剪,甚至可能完全消失。此外,如果它覆盖了两个按钮,那么即使鼠标直接在三角形上方,按钮仍然对鼠标悬停和鼠标点击做出反应。这一切都归功于我们创建子窗口并赋予其鼠标透明度的方式。
private void Window_KeyDown(object sender, KeyEventArgs e)
{
int moveStep = 5;
switch (e.Key)
{
case Key.Up:
_glWnd?.MoveBy(0, -moveStep);
break;
case Key.Down:
_glWnd?.MoveBy(0, moveStep);
break;
case Key.Left:
_glWnd?.MoveBy(-moveStep, 0);
break;
case Key.Right:
_glWnd?.MoveBy(moveStep, 0);
break;
}
}
如我们所见,MoveBy
方法将一个命令加入队列,用于子窗口,最终更新其 ClientLocation
和 ClientSize
。还要注意的是,尽管箭头键将三角形移动到了中心单元格之外,但只要主窗口被调整大小,三角形就会“吸附”回中心单元格!
注意:通过移动窗口来进行平移只是为了展示:
- 我们可以将此类命令发送到窗口
- 主窗口(父窗口)仍然接收键盘输入
- 窗口在到达父窗口边界时会被父窗口裁剪
- 我们具有鼠标透明度,并且可以在窗口覆盖按钮时点击它们
一个真正的(2D)平移要求通过对渲染对象应用平移矩阵来实现会更好!
使用按钮隐藏/显示和控制三角形的旋转
两个按钮的事件处理程序向子窗口发送用于隐藏/显示和停止/恢复三角形旋转的命令。
private void HideShowButton_Checked(object sender, RoutedEventArgs e)
{
_glWnd?.Hide();
}
private void HideShowButton_Unchecked(object sender, RoutedEventArgs e)
{
_glWnd?.Show();
}
private void StopStartButton_Click(object sender, RoutedEventArgs e)
{
_glWnd?.ToggleSpin();
}
隐藏和显示子窗口是否有用,取决于用例。控制渲染参数,如 ToggleSpin
,绝对有用。
结论和关注点
我已展示了如何通过将普通的 OpenTK 窗口直接嵌入 WPF 窗口中,像控件一样利用 WPF 窗口中的 OpenGL 渲染。这不需要任何 DX 互操作,因为 DX 互操作会增加间接层并影响(在我看来,破坏)性能。下载压缩的源代码,构建 VS 解决方案,亲自尝试一下。
关于本文的一些相关主题,还有一些想法,列在下面作为额外的兴趣点。
-
在单独的线程中创建和运行
GameWindow
是 OpenTK 最近才开放的功能,因为它不是跨平台的——在其 GitHub 存储库中有一个关于此的有趣讨论。另一方面,本文是关于 WPF 的,因此我们只针对 Windows。 -
我不太确定的是,如果同时创建多个 OpenGL 渲染区域,每个区域都有自己的窗口,会发生什么。就此而言,似乎
GLWfpControl
本身在多个实例方面存在一些挑战。这些场景我没太大兴趣深入研究,因为对我来说,它们是较不常见的用例。 -
你可能已经注意到,每当使用
GameWinodw
的不安全指针WindowPtr
时,都会出现unsafe
代码块。编译器需要 /unsafe 编译器参数来编译项目,这可以在项目设置中指定。如果你对此感到不适,可以将 OpenGL 相关代码隔离到一个单独的库中,而不影响 WPF 应用程序项目的其余部分。 -
源代码中没有多少防御性编程,例如错误检查,因为这是一个演示应用程序,而不是实际应用。在实际应用中,我们至少应该检查要使用的 OpenGL 版本是否在该系统上受支持。这需要将窗口创建调用放在
try...catch
块中。try { glWnd = new GLWindow(hWndParent, bounds); } catch (GLFWException ex) when (ex.ErrorCode == ErrorCode.VersionUnavailable) { // Error - OpenGL version not supported }
-
关于透明度主题,GLFW 文档指出,GLFW 支持帧缓冲区透明度(可用于创建透明/半透明背景)以及整个窗口透明度。OpenTK 也将此支持带到了 .NET:可以通过调用类似以下的代码来启用帧缓冲区透明度:
GLFW.WindowHint(WindowHintBool.TransparentFramebuffer, true);
或者通过设置NativeWindowSettings
属性TransparentFramebuffer = true
。对于整个窗口透明度,GLFW 提供:GLFW.SetWindowOpacity(WindowPtr, 0.5f); // e.g. half transparency
所以,以上所有内容都很好很真实,但有一个转折:它只适用于顶层窗口,而不适用于子窗口(我还没弄明白为什么)。因此,使用了前面讨论的 Win32 函数
SetLayeredWindowAttributes
。这个函数有一些有趣的功能,也有它自己的局限性。我不会深入探讨它的功能(例如LWA_COLORKEY
与LWA_ALPHA
),但我要提及它的转折,因为它与本文相关:尽管自 Windows 8 起,该函数就支持子窗口,但仅在 Windows 10 及更高版本上才能用于我们的目的。在 Windows 8.1 上,它有一个糟糕的效果,即“释放”子窗口的父窗口边界,也就是说,子窗口可以超出父窗口的边界。此外,透明度似乎仅在使用LWA_ALPHA
时有效,这使整个窗口透明/半透明,但LWA_COLORKEY
无效,其中用于键的颜色仍显示为不透明。因此,最好**不要**在 Windows 8/8.1 上使用窗口透明度,而满足于不透明的背景。SetLayeredWindowAttributes
在 Windows 8.1 上对LWA_COLORKEY
和LWA_ALPHA
的不期望的效果 -
关于**鼠标透明度**主题,OpenTK 可以通过调用类似以下的代码来支持它:
GLFW.WindowHint(WindowHintBool.MousePassthrough, true);
但是,虽然这里使用的 OpenTK 版本具有此 API,但这里使用的底层 OpenGL 版本(3.3)不支持鼠标穿透,因为它只在 3.4 中添加。不过没关系,Win32 函数EnableWindow
会处理这个问题。
实际应用
这是本文解决方案在实际 WPF 应用程序中的低分辨率截图。
OpenGL 渲染仅在钢琴卷帘区域进行,这是一个 OpenGL **窗口**,钢琴的其他部分(如琴键、框架)以及主窗口是 WPF 渲染的内容。图中显示了该区域如何随整个钢琴和主窗口一起移动,即使在平移和缩放的情况下,其行为也与**控件**几乎一样。
历史
- 2024 年 2 月 29 日:初始版本