UE4-桌面宠物/透明窗口

UE交换链默认使用的反转呈现模式,且并未提供更改交换链呈现模式的选项,因此窗口不支持SetLayeredWindowAttributes函数的键透明效果。

(这一点很多unity相关教程上都有提及过需要取消勾选DXGI filp model swapchain选项)

下面说一下代码实现具体思路:

UE创建的窗口不适合分层窗口的键透明效果,那么可以创建一个新窗口,将UE窗口的渲染结果呈现到新窗口上。

UE提供了相应的委托函数,可以通过该函数获取帧渲染结果,然后通过DC绘制到新创建的窗口上。

需要注意,新窗口需要在单独线程上运行,否则会争夺UE窗口的窗口消息,导致UE窗口不再更新。

C++代码:

(因为测试缘故,代码中可能包含了许多不必要的代码,且注释简略,具体函数用途请自行查阅)

(注:添加了托盘窗口相关代码,方便退出程序,自行更改托盘窗口的ico图标路径,或者注释掉)

(注:建议测试时,使用"独立进程的窗口"运行,且阅读完代码下方文章)

// Fill out your copyright notice in the Description page of Project Settings.

using UnrealBuildTool;

public class Game : ModuleRules
{
	public Game(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "Slate", "SlateCore", "ApplicationCore", "Renderer", "RenderCore", "RHI", "ImageWrapper" });

		PrivateDependencyModuleNames.AddRange(new string[] {  });

		// Uncomment if you are using Slate UI
		// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
		
		// Uncomment if you are using online features
		// PrivateDependencyModuleNames.Add("OnlineSubsystem");

		// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
	}
}

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "Engine.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "HAL/PlatformFilemanager.h"
#include <windows/WindowsWindow.h>
#include "Windows/AllowWindowsPlatformTypes.h"
#include <Windows.h>
#include"winuser.h"
#include <shellapi.h>
#include <shellapi.h>
#include "Windows/HideWindowsPlatformTypes.h"

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"

UCLASS()
class GAME_API AMyActor : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AMyActor();
protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
	virtual void Tick(float DeltaTime) override;
public:	
	HWND hwnd;
	static HWND UEhwnd;             //游戏窗口句柄
	static NOTIFYICONDATA nid;      //菜单结构体
	TArray<FColor>outData;   //渲染数据


	//窗口消息函数
	static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
	//渲染回调函数
	void GetRenderBuffer(SWindow& SlateWindow, const FTexture2DRHIRef& BackBuffer);
	//创建窗口
	void CreateAndRunWindow();
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "MyActor.h"

HWND AMyActor::UEhwnd = nullptr;
NOTIFYICONDATA AMyActor::nid;


// ets default values
AMyActor::AMyActor()
{
	PrimaryActorTick.bCanEverTick = true;
}

void AMyActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}

short left=0;

//窗口消息处理函数
LRESULT AMyActor::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    SendNotifyMessageW(UEhwnd, message, wParam, lParam);

    switch (message)
    {
    case WM_PAINT:
    break;


    //托盘图标消息
    case WM_USER + 1:
        if (lParam == WM_RBUTTONUP) 
        {
            HMENU hMenu = CreatePopupMenu();
            AppendMenu(hMenu, MF_STRING, 1001, TEXT("return"));
            POINT pt;
            GetCursorPos(&pt);
            SetForegroundWindow(hwnd);
            TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_BOTTOMALIGN, pt.x, pt.y, 0, hwnd, NULL);
            PostMessageW(hwnd, WM_NULL, 0, 0);
            DestroyMenu(hMenu);
        }
        break;
    //菜单消息
    case WM_COMMAND:
        switch (LOWORD(wParam))
        {
        case 1001:             
            Shell_NotifyIcon(NIM_DELETE, &nid);
            FPlatformMisc::RequestExit(false);
            break;
        }
    case WM_DESTROY:
        PostQuitMessage(0);
        break;

    //游标消息
    case WM_SETCURSOR:
        SetCursor(LoadCursor(NULL, IDC_ARROW));
        break;
    default:
        return DefWindowProc(hwnd, message, wParam, lParam);
    }
    return 0;
}


#include <thread>
void AMyActor::BeginPlay()
{
    Super::BeginPlay();
  
    //创建线程
    std::thread t(&AMyActor::CreateAndRunWindow,this);
    t.detach();


    //获取UE窗口句柄
    TSharedPtr<FGenericWindow> NativeWindow = GEngine->GameViewport->GetWindow()->GetNativeWindow();
    FWindowsWindow* Window = static_cast<FWindowsWindow*>(NativeWindow.Get());
    UEhwnd = Window->GetHWnd();

    //设置窗口
    SetWindowPos(UEhwnd, HWND_TOP, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_HIDEWINDOW);
    SetWindowLong(hwnd, GWL_EXSTYLE, GetWindowLongPtr(hwnd, GWL_EXSTYLE) | WS_EX_TOOLWINDOW|WS_EX_TRANSPARENT);
    SetWindowLong(hwnd, GWL_STYLE, WS_VISIBLE | WS_POPUP);

    //绑定委托
    FSlateApplication::Get().GetRenderer()->OnBackBufferReadyToPresent().AddUObject(this, &AMyActor::GetRenderBuffer);
}

#include "Kismet/KismetSystemLibrary.h"


//回调函数,返回帧渲染结果,以及其对应的窗口句柄
void AMyActor::GetRenderBuffer(SWindow& SlateWindow, const FTexture2DRHIRef& BackBuffer)
{
    //窗口有效,且为主窗口
    if (!GEngine->GameViewport)return;
    TSharedPtr<SWindow> W = GEngine->GameViewport->GetWindow();
    if (&SlateWindow != W.Get())return;


    //获取渲染结果表面宽高
    int Swidth = BackBuffer->GetSizeX();
    int Sheight = BackBuffer->GetSizeY();


    //提取渲染结果
    FIntRect Rect(0, 0, Swidth, Sheight);
    FRHICommandListImmediate& RHICmdList = FRHICommandListExecutor::GetImmediateCommandList();
    RHICmdList.ReadSurfaceData(BackBuffer, Rect, outData, FReadSurfaceDataFlags(RCM_UNorm));

    for (FColor& c : outData)
    {
        c.A = 255;
    }


    //绘制像素
    HDC hdc = GetDC(hwnd);
    BITMAPINFOHEADER bi = { 0 };
    bi.biSize = sizeof(BITMAPINFOHEADER);
    bi.biWidth = Swidth;
    bi.biHeight = -Sheight;
    bi.biPlanes = 1;
    bi.biBitCount = 32;
    bi.biCompression = BI_RGB;
    bi.biSizeImage = Swidth * Sheight * 4;
    SetDIBitsToDevice(hdc, 0, 0, Swidth, Sheight, 0, 0, 0, Sheight, outData.GetData(), (BITMAPINFO*)&bi, DIB_RGB_COLORS);
    ReleaseDC(hwnd, hdc);
}



void AMyActor::CreateAndRunWindow()
{
    //创建窗口
    WNDCLASSEX wc = { sizeof(WNDCLASSEX),0,WndProc,0L, 0L,GetModuleHandle(NULL),NULL,NULL,NULL,NULL,L"Window",NULL };
    RegisterClassEx(&wc);
    hwnd = CreateWindow(TEXT("Window"), TEXT("UE Windows"), WS_OVERLAPPEDWINDOW, 0, 0, 1024, 720, NULL, NULL, wc.hInstance, NULL);
    ShowWindow(hwnd, SW_SHOW);
    UpdateWindow(hwnd);



    //添加托盘图标
    HICON hIcon = (HICON)LoadImage(hInstance, TEXT("D:\\xx.ico"), IMAGE_ICON, 0, 0, LR_LOADFROMFILE);
    nid.cbSize = sizeof(NOTIFYICONDATA);
    nid.hWnd = hwnd;                    
    nid.uID = 1;                         
    nid.hIcon = hIcon;                   
    nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; 
    nid.uCallbackMessage = WM_USER + 1;            
    lstrcpyn(nid.szTip, TEXT("桌面精灵"), sizeof(nid.szTip));
    Shell_NotifyIcon(NIM_ADD, &nid);




    //设置窗口
    SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_SHOWWINDOW);
    SetWindowLong(hwnd, GWL_EXSTYLE, GetWindowLongPtr(hwnd, GWL_EXSTYLE) | WS_EX_LAYERED|WS_EX_TOOLWINDOW);
    SetWindowLong(hwnd, GWL_STYLE, WS_VISIBLE | WS_POPUP);
    SetLayeredWindowAttributes(hwnd, RGB(0, 0, 0), 0, LWA_COLORKEY);


    MSG msg = { 0 };
    while (GetMessageW(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);  
        DispatchMessage(&msg);  
    }

}


创建后期处理材质:

作用就是将距离屏幕过远的像素设置为键透明的颜色,允许Win进行窗口表面合并时进行相关处理

建议将透明键设置为一个非常用颜色,黑色的话会把物体阴影给透明掉,而且会有一些明显的噪点。

注意一下颜色的换算,UE材质内的(0-1)=RGB颜色的(0-255)

应用材质,并勾选无限范围

这个时候差不多就可以了,以下是运行效果

右键托盘图标可以关闭程序,若路径下无图标文件,则会显示透明图标。

文本内容使用英文,否则会乱码。

如果不使用"独立进程的窗口运行",这一步会使编辑器退出,相关问题可以去改源码。

关于性能问题:

ReadSurfaceData()这个函数占用了约25ms,而其它代码总共加起来也只有3-4ms,如果加上Game线程外的一些耗时,最终的测试帧率只有40fps上下。

ReadSurfaceData函数网上有优化教程,看介绍,大概能优化到10ms左右。

不过相较于这样,我更倾向于为新窗口分配表面,这样可以避免两设备之间的内存拷贝操作。

结尾:

剩下的就是自己去写移动,交互相关的逻辑了,下面给一个简单的案例:

操作:点击窗口非透明部分,且储蓄按住鼠标左键,可进行拖动,亦可以在按住过程中滚动鼠标滚轮

(上面的程序有一些鼠标交互的小问题,只有鼠标在窗口的非透明区域,才能接收到按键消息,否则只能接收到鼠标位移消息,这个可以自己去改,然后鼠标灵敏度适配相关功能也需要自己去做)

Pawn(代替character,控制相机矩阵的位置变幻)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值