使Win32窗口透明的几种方法

文章介绍了在Windows应用程序中创建透明窗口的三种方法:DirectComposition、DWMAPI和分层窗口,讨论了它们的优缺点,如DirectComposition效率高但不支持点击穿透,DWMAPI简单但需系统开启DWM,而分层窗口支持点击穿透但不利用GPU加速。文章还提供了代码示例和建议,帮助开发者选择适合的透明窗口实现方案。
摘要由CSDN通过智能技术生成

在制作某些如桌面挂件、桌宠、浮窗类应用程序时,可能会需要创建透明窗口,比较简单的有使用色键、设置窗体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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值