在制作某些如桌面挂件、桌宠、浮窗类应用程序时,可能会需要创建透明窗口,比较简单的有使用色键、设置窗体Alpha等(都是通过调用SetLayeredWindowAttributes),但这两种方法太古老,而且效果也不好:
色键支持点击穿透,但不支持半透明,即只有完全透明和完全不透明两个值,会有锯齿边,一般是用来做异形窗口而不是透明窗口的,多见于播放器和登录界面;
窗体Alpha不支持局部设置;
没有用到硬件加速,效率太慢。
在GPU普及的当下显然不合适,所以得需要既能支持硬件加速,又能支持半透明效果的方法,最好也要支持点击穿透。好在微软经过了这么多年,这么多版本Windows的发展已经意识到了这种需求(当年XP到Vista的过渡时期网上有一大把的桌面美化工具),而且有了相应的API可以给我们使用了,效果也都不错,Aero桌面就是最好的例子。
目前已知有3种API可以创建透明窗口:DirectComposition, DWM API, 分层窗口。
代码准备
以Visual Studio 2017+DirectX11为例,在创建项目时选择“Windows桌面应用程序”的模板,IDE会自动生成一个创建空白窗口的代码:
创建项目
然后在项目上右键属性,清单工具,输入和输出,把DPI识别功能改为高DPI或每个监视器高DPI识别,表示程序支持高分屏;在链接器,输入,附加依赖项中添加d3d11.lib,d3dcompiler.lib。
考虑到这类程序通常没有边框、菜单,而且会把窗口置顶,因此把CreateWindow创建窗口那一步修改为:
HWND hWnd = CreateWindowExW(WS_EX_TOPMOST, szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
SetWindowLong(hWnd, GWL_STYLE, WS_MINIMIZEBOX | WS_SYSMENU);
把hWnd保存到一个全局变量(假设为g_hwnd),然后把窗口类wcex.lpszMenuName改为NULL。
添加3D绘图操作:
创建一个着色器文件,命名为“shader.hlsl”,然后右键该文件属性,把“从生成中排除”选为“是”;打开RC资源文件,导入该着色器文件,类型设为HLSL,ID设为IDR_HLSL_SHADER:(为了节约行数写得比较紧密)
struct VertexInput{float3 pos:POSITION;float4 color:COLOR;};
struct VertexOutput{float4 pos:SV_POSITION;float4 color:COLOR;};
VertexOutput VSmain(VertexInput v){
VertexOutput vout;
vout.pos = float4(v.pos, 1.0f);
vout.color = v.color;
return vout;
}
float4 PSmain(VertexOutput v) : SV_TARGET{
return v.color;
}
创建一个头文件“3d.h”
#pragma once
#include <Windows.h>
int GameWidth();
int GameHeight();
int Init3D(HWND hwnd);
void Uninit3D();
int Loop3D(HWND hwnd);
int Resize3D(HWND hwnd,int w, int h);
创建“3d.cpp”,包含绘制三角形的代码:
#include "3d.h"
#include <d3d11.h>
#include <d3dcompiler.h>
#include <wrl/client.h>
#include <assert.h>
#include <vector>
#include <DirectXColors.h>
#include "resource.h"
#ifdef _DEBUG
#define CHECK_HR(x) assert(SUCCEEDED(x))
#define CHECK_INT(x) assert(x==0)
#define CHECK_BOOL(x) assert(x==TRUE)
#else
#define CHECK_HR(x) x
#define CHECK_INT(x) x
#define CHECK_BOOL(x) x
#endif
using Microsoft::WRL::ComPtr;
int gameWidth = 800, gameHeight = 600;
int GameWidth()
{
return gameWidth;
}
int GameHeight()
{
return gameHeight;
}
ComPtr<IDXGISwapChain>swapChain;
ComPtr<ID3D11Device>d3ddevice;
ComPtr<ID3D11DeviceContext>ctx;
ComPtr<ID3D11RenderTargetView>rtv;
ComPtr<ID3D11DepthStencilView>dsv;
ComPtr<ID3D11VertexShader>vshader;
ComPtr<ID3D11PixelShader>pshader;
ComPtr<ID3D11InputLayout>inputLayout;
const D3D11_INPUT_ELEMENT_DESC inputLayoutDesc[2] = {
{"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0},
{"COLOR",0,DXGI_FORMAT_R32G32B32A32_FLOAT,0,12,D3D11_INPUT_PER_VERTEX_DATA,0},
};
struct VertexInput
{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT4 color;
};
int InitShaders()
{
HRSRC hRsrc = FindResource(GetModuleHandle(NULL), MAKEINTRESOURCE(IDR_HLSL_SHADER), L"hlsl");
HGLOBAL hRes = LoadResource(GetModuleHandle(NULL), hRsrc);
LPVOID ptr = LockResource(hRes);
size_t sz = SizeofResource(GetModuleHandle(NULL), hRsrc);
int cf = 0;
#ifdef _DEBUG
cf |= D3DCOMPILE_DEBUG;
#endif
ComPtr<ID3DBlob>errors;
ComPtr<ID3DBlob>vsbinary, psbinary;
if (FAILED(D3DCompile(ptr, sz, NULL, NULL, NULL, "VSmain", "vs_4_0", cf, NULL, &vsbinary, &errors)))
FatalAppExitA(NULL, (LPSTR)errors->GetBufferPointer());
if (FAILED(D3DCompile(ptr, sz, NULL, NULL, NULL, "PSmain", "ps_4_0", cf, NULL, &psbinary, &errors)))
FatalAppExitA(NULL, (LPSTR)errors->GetBufferPointer());
FreeResource(hRes);
CHECK_HR(d3ddevice->CreateVertexShader(vsbinary->GetBufferPointer(), vsbinary->GetBufferSize(), NULL, &vshader));
CHECK_HR(d3ddevice->CreatePixelShader(psbinary->GetBufferPointer(), psbinary->GetBufferSize(), NULL, &pshader));
CHECK_HR(d3ddevice->CreateInputLayout(inputLayoutDesc, ARRAYSIZE(inputLayoutDesc), vsbinary->GetBufferPointer(), vsbinary->GetBufferSize(), &inputLayout));
return 0;
}
int InitDevice(HWND hwnd)
{
//创建
DEVMODE dm;
CHECK_BOOL(EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &dm));
DXGI_SWAP_CHAIN_DESC scdesc{};
scdesc.BufferCount = 2;
scdesc.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
scdesc.BufferDesc.Height = GameHeight();
scdesc.BufferDesc.Width = GameWidth();
scdesc.BufferDesc.RefreshRate.Numerator = dm.dmDisplayFrequency;
scdesc.BufferDesc.RefreshRate.Denominator = 1;
scdesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
scdesc.SampleDesc.Count = 1;
scdesc.SampleDesc.Quality = 0;
scdesc.OutputWindow = hwnd;
scdesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;//不能使用带有FLIP的参数,会导致透明度丢失
scdesc.Windowed = TRUE;
scdesc.Flags = DXGI_SWAP_CHAIN_FLAG_GDI_COMPATIBLE;
UINT f = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#ifdef _DEBUG
f |= D3D11_CREATE_DEVICE_DEBUG;
#endif
CHECK_HR(D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, f, NULL, 0, D3D11_SDK_VERSION, &scdesc, &swapChain, &d3ddevice, NULL, &ctx));
return 0;
}
void UninitDevice()
{
swapChain.Reset();
ctx.Reset();
d3ddevice.Reset();
}
int InitResource()
{
ComPtr<ID3D11Texture2D>backBuffer,depthStencil;
CHECK_HR(swapChain->GetBuffer(0, IID_PPV_ARGS(&backBuffer)));
CHECK_HR(d3ddevice->CreateRenderTargetView(backBuffer.Get(), NULL, &rtv));
CD3D11_TEXTURE2D_DESC depthStencilDesc(DXGI_FORMAT_D24_UNORM_S8_UINT, GameWidth(), GameHeight(), 1, 1, D3D11_BIND_DEPTH_STENCIL);
CHECK_HR(d3ddevice->CreateTexture2D(&depthStencilDesc, NULL, &depthStencil));
CD3D11_DEPTH_STENCIL_VIEW_DESC depthStencilViewDesc(D3D11_DSV_DIMENSION_TEXTURE2D);
CHECK_HR(d3ddevice->CreateDepthStencilView(depthStencil.Get(), &depthStencilViewDesc, &dsv));
CD3D11_VIEWPORT vp(0.0f, 0.0f, GameWidth(), GameHeight());
ctx->RSSetViewports(1, &vp);
D3D11_BLEND_DESC blendDesc{};
blendDesc.RenderTarget[0].BlendEnable = TRUE;
blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
blendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
blendDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
blendDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO;
blendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
blendDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
ComPtr<ID3D11BlendState>abd;
CHECK_HR(d3ddevice->CreateBlendState(&blendDesc, abd.GetAddressOf()));
ctx->OMSetBlendState(abd.Get(), NULL, 0xFFFFFFFF);
return 0;
}
void UninitResource()
{
dsv.Reset();
rtv.Reset();
}
int Recreate3D(HWND hwnd)
{
UninitResource();
UninitDevice();
CHECK_INT(InitDevice(hwnd));
CHECK_INT(InitResource());
return 0;
}
int Init3D(HWND hwnd)
{
CHECK_INT(InitDevice(hwnd));
CHECK_INT(InitShaders());
CHECK_INT(InitResource());
return 0;
}
void Uninit3D()
{
UninitResource();
UninitDevice();
}
void DrawTriangle3D(VertexInput*vertices, int c)
{
D3D11_BUFFER_DESC vbd{};
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = c * sizeof(VertexInput);
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
D3D11_SUBRESOURCE_DATA InitData{};
InitData.pSysMem = vertices;
ComPtr<ID3D11Buffer>vBuffer;
CHECK_HR(d3ddevice->CreateBuffer(&vbd, &InitData, vBuffer.GetAddressOf()));
UINT stride = sizeof(VertexInput);
UINT offset = 0;
ctx->IASetVertexBuffers(0, 1, vBuffer.GetAddressOf(), &stride, &offset);
ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
ctx->IASetInputLayout(inputLayout.Get());
ctx->VSSetShader(vshader.Get(), NULL, 0);
ctx->PSSetShader(pshader.Get(), NULL, 0);
ctx->Draw(c, 0);
}
void Draw3D(HWND hwnd)
{
//清空
ctx->ClearRenderTargetView(rtv.Get(), DirectX::Colors::Transparent);
ctx->ClearDepthStencilView(dsv.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
//绘制
using namespace DirectX;
POINT mp;
GetCursorPos(&mp);
ScreenToClient(hwnd, &mp);
float dy = 1.0f - 2.0f * mp.y / GameHeight();
VertexInput pt[] = {
{ XMFLOAT3(0, 0.5f + dy, 0), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },
{ XMFLOAT3(0.5f, dy - 0.5f, 0), XMFLOAT4(0.0f, 0.0f, 1.0f, 0.0f) },
{ XMFLOAT3(-0.5f, dy - 0.5f, 0), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) }
};
DrawTriangle3D(pt, ARRAYSIZE(pt));
}
int Loop3D(HWND hwnd)
{
ctx->OMSetRenderTargets(1, rtv.GetAddressOf(), dsv.Get());
Draw3D(hwnd);
HRESULT hr=swapChain->Present(1, 0);
if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET)
Recreate3D(hwnd);
else
CHECK_HR(hr);
return 0;
}
int Resize3D(HWND hwnd,int w, int h)
{
if (w == 0 || h == 0)
return 0;
gameWidth = w;
gameHeight = h;
if (!swapChain)
return 1;
UninitResource();
DXGI_SWAP_CHAIN_DESC desc;
CHECK_HR(swapChain->GetDesc(&desc));
HRESULT hr = swapChain->ResizeBuffers(desc.BufferCount, GameWidth(), GameHeight(), desc.BufferDesc.Format, desc.Flags);
if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET)
Recreate3D(hwnd);
else
CHECK_HR(hr);
CHECK_INT(InitResource());
return 0;
}
若使用其他绘图API注意把清空底色设为透明色。
回到创建窗口的代码文件中,开头添加#include"3d.h",把消息循环部分改为:
MSG msg{};
if (Init3D(g_hwnd))
return 1;
while (msg.message != WM_QUIT)
{
if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
else
{
Loop3D(g_hwnd);
}
}
Uninit3D();
然后运行,应该就能看到一个三角形跟着鼠标位置在竖直方向上移动,关闭时需要按下Alt+F4。
下面说说3种透明窗口的API。
1. DirectComposition
这个方法需要用到以下API:
WS_EX_NOREDIRECTIONBITMAP(以按位或加到CreateWindowEx的第一个参数)
CreateSwapChainForComposition
DCompositionCreateDevice
IDCompositionVisual::SetContent
IDCompositionTarget::SetRoot
该方法绕过了窗口后表面的合成过程,效率会高一些,但是只有Win8及以后的操作系统才能用(包括微软建议用的Windows.UI.Composition),不支持点击穿透,而且需要改动的代码较多,不是很推荐,因此不再详细展开讲了。
2. DWM API(强烈推荐使用该方法)
该方法可用下面两个中的任一API:(需要#include<dwmapi.h>,在链接库中添加“dwmapi.lib”)
DwmEnableBlurBehindWindow
只需在创建窗口的代码文件中添加下列代码即可:
DWM_BLURBEHIND db{};
db.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
db.hRgnBlur = CreateRectRgn(0, 0, -1, -1);
db.fEnable = TRUE;
DwmEnableBlurBehindWindow(g_hwnd, &db);
效果如图:
透明窗口
这个API据微软文档说在Win7系统上会让窗口背景产生模糊而非透明,因此建议用下面的API。
DwmExtendFrameIntoClientArea
在创建窗口的代码文件中添加下面两行:
MARGINS margins={-1};
DwmExtendFrameIntoClientArea(g_hwnd,&margins);
可以看到这个更简单,效果同上。这两个API的缺点是不能做点击穿透,需要系统开启DWM,仅支持Vista及以后的系统,但胜在简单,因此如果你的程序近似矩形或者不需要点击穿透那就用这个方法。两个API的区别只在窗口有边框的时候才可以看到,第一个窗口是透明的但是有边框,第二个窗口不是透明的(在Win7上是一整个毛玻璃),见图:
左边使用第一个API,右边使用第二个
3. 分层窗口
使用下面的API:
WS_EX_LAYERED(以按位或加到CreateWindowEx的第一个参数)
UpdateLayeredWindow(使用ULW_ALPHA参数)
在“3d.cpp”创建交换链的代码中将scdesc.Flags设为DXGI_SWAP_CHAIN_FLAG_GDI_COMPATIBLE,在Loop3D函数中,Draw3D与Present之间添加下列代码:
if (GetWindowLong(hwnd, GWL_EXSTYLE)&WS_EX_LAYERED)
{
ComPtr<IDXGISurface1>surface;
swapChain->GetBuffer(0, IID_PPV_ARGS(&surface));
HDC hdcSurface, hdcWindow = GetDC(hwnd);
surface->GetDC(FALSE, &hdcSurface);
POINT ptOrigin{};
DXGI_SURFACE_DESC desc;
surface->GetDesc(&desc);
SIZE szOrigin{ desc.Width,desc.Height };
BLENDFUNCTION blend{ AC_SRC_OVER,0,255,AC_SRC_ALPHA };
//szOrigin参数不能超出surface的尺寸
UpdateLayeredWindow(hwnd, hdcWindow, NULL, &szOrigin, hdcSurface, &ptOrigin, 0, &blend, ULW_ALPHA);
surface->ReleaseDC(NULL);
ReleaseDC(hwnd, hdcWindow);
}
该方法的优点是应用范围广,支持Win2000及以后的系统,且(我查了Direct3D9的文档发现IDirect3DSurface9::GetDC函数不支持透明度,因此不能用来创建透明窗口,不支持XP及以前的系统,必须要用Direct3D10以上API)支持Alpha为0处的点击穿透,满足了本文前面提到的所有要求,但是因为出现了HDC的操作,无法利用GPU加速(UpdateLayeredWindow是在CPU上完成的),拖慢了运行效率。因此在使用这种方法前应仔细考虑取舍。
效果如图:
鼠标变成I字形表示具有点击穿透效果
PS. 使窗口可移动
由于前面把窗口边框都移除了,移动窗口又成了问题,需要添加代码使窗口可移动:
void ProcessMouseDrag(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static POINT posMouseClick;
switch (message)
{
case WM_LBUTTONDOWN:
SetCapture(hWnd);
posMouseClick.x = LOWORD(lParam);
posMouseClick.y = HIWORD(lParam);
break;
case WM_LBUTTONUP:ReleaseCapture(); break;
case WM_MOUSEMOVE:
if (GetCapture() == hWnd)
{
RECT rWindow;
GetWindowRect(hWnd, &rWindow);
SetWindowPos(hWnd, NULL, rWindow.left + (short)LOWORD(lParam) - posMouseClick.x,
rWindow.top + (short)HIWORD(lParam) - posMouseClick.y, 0, 0, SWP_NOSIZE | SWP_NOZORDER);
}
break;
}
}
在窗口过程回调函数的开始处调用上面的函数:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
ProcessMouseDrag(hWnd, message, wParam, lParam);
//……其他处理
return 0;
}
然后拖动窗口上任一处有图像的地方就可以移动了。
PS. 在其他语言中使用
若你用的是其他语言,比如C#(Unity)等,需要能够调用到上述API,因为这些API微软只开放了C/C++的版本,并使用支持透明的绘图库(GDI+/D2D/D3D/GL/Vulkan等)。
这里提供一个封装好的采用DWM方式的DLL和示例程序,使用时直接调用CMEnableDWMTransparent(1)就可以了,而且还提供了移动窗口的函数,方便开发:https://pan.baidu.com/s/14PlMTCYNV_5ahVbngW0CAg
Unity程序开启DWM透明
可以看到同样能够实现半透明的效果,同时左下角按钮是不透明的(表示可以局部设置透明度)。由于Windows要求带有Alpha的颜色必须是预乘的,当RGB的值大于Alpha时会产生这种比较亮的修改颜色通道的效果。 作者:lxfly2000 https://www.bilibili.com/read/cv18607214 出处:bilibili