├── EVD安装包_V0.1
├── .nomedia
└── EVDsetup.exe
├── README.md
├── main.cpp
├── 一键关闭虚拟屏
├── .nomedia
└── STOP EasyVirtualDisplay.bat
└── 后台启动脚本
├── .nomedia
└── START MIN EasyVirtualDisplay.bat
/EVD安装包_V0.1/.nomedia:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/EVD安装包_V0.1/EVDsetup.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprlightning/Easy-Virtual-Display-AutoRunBackground/b49fa35fe4ebc48cd55d7c926c0273cac55b4542/EVD安装包_V0.1/EVDsetup.exe
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > [!NOTE]
2 | > This project is only a temporary solution and currently only provides minimal maintenance. Parsec-vdd will directly provide a more complete solution, please continue to pay attention.
3 | >
4 | > where find it? 👉👉👉https://github.com/nomi-san/parsec-vdd
5 | >
6 | >———————————————————本项目fork源于KtzeAbyss——————————————————
7 | >
8 | > KtzeAbyss的项目我没权限上传,所以在此fork后打包发布。
9 | >
10 | > 在KtzeAbyss版本的基础上添加了适用于开机自启的后台静默运行脚本(受jiuyan660启发,找到了开启虚拟屏的源头程序)。
11 | >
12 | > 要求将脚本放在EVD安装目录(不可以安装在C盘),并且关闭UAC,开启白名单,创建计划选无论是否登录都要运行,不保存密码,勾选最高权限,触发器用任何用户登录时。
13 | >
14 | > 一键关闭脚本可一键关闭虚拟屏。
15 | >
16 | > 所有内容见release。
17 | >
18 | > 
19 | >
20 | >
21 | > PS.KtzeAbyss的项目地址👉👉👉https://github.com/KtzeAbyss/Easy-Virtual-Display
22 | >
23 | >———————————————————以下为历史内容——————————————————
24 | >
25 | > 本项目仅作为一个临时解决方案,目前只提供最低限度的维护,Parsec-vdd将直接提供更完善的解决方案,请各位持续关注。
26 | >
27 | > 项目地址 👉👉👉https://github.com/nomi-san/parsec-vdd
28 |
29 | [English](#english) | [中文](#中文)
30 |
31 |
32 |
33 | # Easy Virtual Display
34 |
35 | Create virtual displays in Windows with ease, supporting a range of resolutions and refresh rates (such as 4K 240Hz). Ideal for remote control or graphics card spoofing.
36 |
37 | # Project Background
38 |
39 | This project builds upon the ParsecVDD foundation and utilizes the repository found at [https://github.com/nomi-san/parsec-vdd](https://github.com/nomi-san/parsec-vdd).
40 |
41 | # Download
42 |
43 | Please select the latest release version.
44 |
45 | 
46 |
47 |
48 |
49 | # How to Use
50 |
51 | 1. Download and install the application. It is recommended to create a shortcut.
52 |
53 |
54 |
55 | 
56 |
57 |
58 | 
59 |
60 |
61 | 2. Double-click to launch (virtualDisplayLit.exe, please make sure to run in administrator mode).
62 | 3. The program will by default hide in the system tray at the bottom right corner, Right-click the icon to access the feature menu.On the initial run, install the driver (only required the first time).
63 | 4. Then, click 'Start Virtual Display' to access the display settings via right-click on the desktop, just like configuring a physical monitor.
64 |
65 | 
66 |
67 | ## Menu Items
68 | From top to bottom, the menu options are as follows: Start Virtual Display, Stop Virtual Display, Force Quit, Install Driver, Uninstall Driver, and Exit.
69 | 1. Start Virtual Display
70 | 2. Stop Virtual Display
71 | 3. Force Quit
72 | 4. Install Driver
73 | 5. Uninstall Driver
74 | 6. Exit
75 |
76 | # Demo
77 |
78 | ## Privacy Screen (Remote Control/Streaming)
79 |
80 | Privacy Screen (Remote Control/Streaming): After starting the virtual display, configure it to display only on Display 2 (Virtual Display) in the display settings. This will cause the host machine's screen to go black while the client machine displays the host's screen, allowing you to work discreetly without being detected by others.
81 |
82 | ## Overcoming Physical Display Limitations
83 | Unrestricted creation of virtual displays with various resolutions and refresh rates, allowing the client to output user-preferred resolutions and refresh rates (such as 4K 240Hz) on low-performance displays or systems without a physical display.
84 |
85 |
86 |
87 | # Easy Virtual Display(简易虚拟显示器)
88 |
89 | 轻松在Windows中创建虚拟显示器,支持各种分辨率和刷新率(如4k 244hz)。非常适用于远程控制或图形卡欺骗。
90 |
91 | # 项目背景
92 |
93 | 本项目基于ParsecVDD的基础构建,并利用了位于[https://github.com/nomi-san/parsec-vdd](https://github.com/nomi-san/parsec-vdd)的存储库。
94 |
95 | # 下载
96 |
97 | 请选择最新发布版本。
98 |
99 |
100 | 
101 |
102 |
103 | # 使用方法(首次启动务必先安装驱动!!!首次启动务必先安装驱动!!!首次启动务必先安装驱动!!!)
104 |
105 | 1. 下载并安装应用程序。建议创建快捷方式。
106 |
107 |
108 | 
109 |
110 | 
111 |
112 | 2. 双击启动(virtualDisplayLit.exe),请确保以管理员模式运行。
113 | 3. 该程序默认隐藏在右下角系统托盘中,右键单击图标即可访问功能菜单。首次运行时,请安装驱动程序(仅首次运行)。
114 | 4. 然后,单击“启动虚拟显示器”以通过在桌面上右键单击来访问显示设置,就像配置物理显示器一样。
115 |
116 | 
117 |
118 | ## 菜单项
119 |
120 | 从上到下,菜单选项如下:启动虚拟显示器、停止虚拟显示器、强制退出、安装驱动、卸载驱动和退出。
121 | 1. 启动虚拟显示器:正常启动(首次运行前务必安装驱动)
122 | 2. 停止虚拟显示器:正常停止
123 | 3. 强制退出:某些情况下驱动可能会出现占用导致虚拟显示器启动不正常,此时可以先启用强制退出功能,确保驱动异常占用不是本程序导致的,然后卸载重装驱动或修复驱动
124 | 4. 安装驱动:首次启动务必先安装驱动!!!首次启动务必先安装驱动!!!首次启动务必先安装驱动!!!
125 | 5. 卸载驱动:卸载驱动
126 | 6. 退出:退出程序
127 |
128 | # 玩法演示
129 |
130 | ## 开机后台自启动
131 | 要求将脚本放在EVD安装目录(不可以安装在C盘),并且关闭UAC,开启白名单,创建计划选无论是否登录都要运行,不保存密码,勾选最高权限,触发器用任何用户登录时。
132 |
133 |
134 | 
135 |
136 |
137 | 
138 |
139 |
140 | ## 隐私屏(远程控制/串流)
141 |
142 | 启动虚拟显示器后,在显示设置中设置仅在显示器2(虚拟显示器)上显示,此时被控端(host)将黑屏,控制端(client)将正常显示被控端(host)的画面,允许你成为卷王而不被其他人发现。
143 |
144 | ## 摆脱物理显示器限制
145 |
146 | 无限制的创建各种分辨率和各种刷新率的虚拟显示器,允许被控端在低性能显示器或无显示器搭载的情况下,控制端输出用户喜好的分辨率和刷新率(如4K 240Hz)。
147 |
148 | # Star History
149 |
150 | [](https://star-history.com/#KtzeAbyss/Easy-Virtual-Display&Date)
151 |
152 |
--------------------------------------------------------------------------------
/main.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 |
10 | #pragma comment(lib, "Setupapi.lib")
11 |
12 | BOOLEAN GetDevicePath2(
13 | _In_ LPCGUID InterfaceGuid,
14 | _Out_writes_(BufLen) PTCHAR DevicePath,
15 | _In_ size_t BufLen
16 | )
17 | {
18 | HANDLE hDevice = INVALID_HANDLE_VALUE;
19 | PSP_DEVICE_INTERFACE_DETAIL_DATA deviceInterfaceDetailData = NULL;
20 | ULONG predictedLength = 0;
21 | ULONG requiredLength = 0;
22 | HDEVINFO hardwareDeviceInfo;
23 | SP_DEVICE_INTERFACE_DATA deviceInterfaceData = { 0 };
24 | BOOLEAN status = FALSE;
25 | HRESULT hr;
26 |
27 | hardwareDeviceInfo = SetupDiGetClassDevs(
28 | InterfaceGuid,
29 | NULL, // Define no enumerator (global)
30 | NULL, // Define no
31 | (DIGCF_PRESENT | // Only Devices present
32 | DIGCF_DEVICEINTERFACE)); // Function class devices.
33 | if (INVALID_HANDLE_VALUE == hardwareDeviceInfo)
34 | {
35 | printf("Idd device: SetupDiGetClassDevs failed, last error 0x%x\n", GetLastError());
36 | return FALSE;
37 | }
38 |
39 | deviceInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
40 |
41 |
42 | if (!SetupDiEnumDeviceInterfaces(hardwareDeviceInfo,
43 | 0, // No care about specific PDOs
44 | InterfaceGuid,
45 | 0, //
46 | &deviceInterfaceData))
47 | {
48 | printf("Idd device: SetupDiEnumDeviceInterfaces failed, last error 0x%x\n", GetLastError());
49 | goto Clean0;
50 | }
51 |
52 | //
53 | // Allocate a function class device data structure to receive the
54 | // information about this particular device.
55 | //
56 | SetupDiGetDeviceInterfaceDetail(
57 | hardwareDeviceInfo,
58 | &deviceInterfaceData,
59 | NULL, // probing so no output buffer yet
60 | 0, // probing so output buffer length of zero
61 | &requiredLength,
62 | NULL);//not interested in the specific dev-node
63 |
64 | if (ERROR_INSUFFICIENT_BUFFER != GetLastError())
65 | {
66 | printf("Idd device: SetupDiGetDeviceInterfaceDetail failed, last error 0x%x\n", GetLastError());
67 | goto Clean0;
68 | }
69 |
70 | predictedLength = requiredLength;
71 | deviceInterfaceDetailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA)HeapAlloc(
72 | GetProcessHeap(),
73 | HEAP_ZERO_MEMORY,
74 | predictedLength
75 | );
76 |
77 | if (deviceInterfaceDetailData)
78 | {
79 | deviceInterfaceDetailData->cbSize =
80 | sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
81 | }
82 | else
83 | {
84 | printf("Idd device: HeapAlloc failed, last error 0x%x\n", GetLastError());
85 | goto Clean0;
86 | }
87 |
88 | if (!SetupDiGetDeviceInterfaceDetail(
89 | hardwareDeviceInfo,
90 | &deviceInterfaceData,
91 | deviceInterfaceDetailData,
92 | predictedLength,
93 | &requiredLength,
94 | NULL))
95 | {
96 | printf("Idd device: SetupDiGetDeviceInterfaceDetail failed, last error 0x%x\n", GetLastError());
97 | goto Clean1;
98 | }
99 |
100 | hr = StringCchCopy(DevicePath, BufLen, deviceInterfaceDetailData->DevicePath);
101 | if (FAILED(hr))
102 | {
103 | printf("Error: StringCchCopy failed with HRESULT 0x%x", hr);
104 | status = FALSE;
105 | goto Clean1;
106 | }
107 | else
108 | {
109 | status = TRUE;
110 | }
111 |
112 | Clean1:
113 | (VOID)HeapFree(GetProcessHeap(), 0, deviceInterfaceDetailData);
114 | Clean0:
115 | (VOID)SetupDiDestroyDeviceInfoList(hardwareDeviceInfo);
116 | return status;
117 | }
118 |
119 | HANDLE DeviceOpenHandle(const GUID& devGuid)
120 | {
121 | // const int maxDevPathLen = 256;
122 | TCHAR devicePath[256] = { 0 };
123 | HANDLE hDevice = INVALID_HANDLE_VALUE;
124 | do
125 | {
126 | if (FALSE == GetDevicePath2(
127 | &devGuid,
128 | devicePath,
129 | sizeof(devicePath) / sizeof(devicePath[0])))
130 | {
131 | break;
132 | }
133 | if (_tcslen(devicePath) == 0)
134 | {
135 | printf("GetDevicePath got empty device path\n");
136 | break;
137 | }
138 |
139 | _tprintf(_T("Idd device: try open %s\n"), devicePath);
140 | hDevice = CreateFile(
141 | devicePath,
142 | GENERIC_READ | GENERIC_WRITE,
143 | // FILE_SHARE_READ | FILE_SHARE_WRITE,
144 | 0,
145 | NULL, // no SECURITY_ATTRIBUTES structure
146 | OPEN_EXISTING, // No special create flags
147 | 0, // No special attributes
148 | NULL
149 | );
150 | if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL)
151 | {
152 | DWORD error = GetLastError();
153 | printf("CreateFile failed 0x%lx\n", error);
154 | }
155 | } while (0);
156 |
157 | return hDevice;
158 | }
159 |
160 | enum VddCtlCode
161 | {
162 | IOCTL_VDD_CONNECT = 0x22A008,
163 | IOCTL_VDD_ADD = 0x22E004,
164 | IOCTL_VDD_UPDATE = 0x22A00C,
165 | };
166 |
167 | void VddIoCtl(HANDLE vdd, VddCtlCode code)
168 | {
169 | BYTE InBuffer[32]{};
170 | int OutBuffer = 0;
171 | OVERLAPPED Overlapped{};
172 | DWORD NumberOfBytesTransferred;
173 |
174 | Overlapped.hEvent = CreateEventW(NULL, NULL, NULL, NULL);
175 | DeviceIoControl(vdd, code, InBuffer, _countof(InBuffer), &OutBuffer, sizeof(OutBuffer), NULL, &Overlapped);
176 | GetOverlappedResult(vdd, &Overlapped, &NumberOfBytesTransferred, TRUE);
177 |
178 | if (Overlapped.hEvent && Overlapped.hEvent != INVALID_HANDLE_VALUE)
179 | CloseHandle(Overlapped.hEvent);
180 | }
181 |
182 | bool quit = false;
183 |
184 | void pipe_thread()
185 | {
186 | HANDLE hPipe;
187 | char buffer[1024] = { 0 };
188 | DWORD dwRead;
189 |
190 | hPipe = CreateNamedPipe(TEXT("\\\\.\\pipe\\MyPipe"),
191 | PIPE_ACCESS_DUPLEX,
192 | PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
193 | 1,
194 | 1024 * 16,
195 | 1024 * 16,
196 | NMPWAIT_USE_DEFAULT_WAIT,
197 | NULL);
198 |
199 | if (hPipe != INVALID_HANDLE_VALUE)
200 | {
201 | if (ConnectNamedPipe(hPipe, NULL) != FALSE) // wait for someone to connect to the pipe
202 | {
203 | while (ReadFile(hPipe, buffer, sizeof(buffer) - 1, &dwRead, NULL) != FALSE)
204 | {
205 | buffer[dwRead] = '\0';
206 |
207 | if (std::string(buffer) == "quit")
208 | {
209 | quit = true;
210 | break;
211 | }
212 | }
213 | }
214 |
215 | DisconnectNamedPipe(hPipe);
216 | }
217 |
218 | CloseHandle(hPipe);
219 | }
220 |
221 | int main()
222 | {
223 | const GUID PARSEC_VDD_DEVINTERFACE = \
224 | { 0x00b41627, 0x04c4, 0x429e, { 0xa2, 0x6e, 0x02, 0x65, 0xcf, 0x50, 0xc8, 0xfa } };
225 |
226 | // try to get device handle with GUID
227 | HANDLE vdd = DeviceOpenHandle(PARSEC_VDD_DEVINTERFACE);
228 | if (!vdd || vdd == INVALID_HANDLE_VALUE)
229 | {
230 | printf("failed to get ParsecVDD device handle.\n");
231 | return 1;
232 | }
233 |
234 | // connect & plug in
235 | VddIoCtl(vdd, IOCTL_VDD_CONNECT);
236 | VddIoCtl(vdd, IOCTL_VDD_UPDATE);
237 | VddIoCtl(vdd, IOCTL_VDD_ADD);
238 | VddIoCtl(vdd, IOCTL_VDD_UPDATE);
239 |
240 | std::thread pipe_thread_instance(pipe_thread);
241 |
242 | while (true)
243 | {
244 | // update each 100ms
245 | std::this_thread::sleep_for(std::chrono::milliseconds(100));
246 | VddIoCtl(vdd, IOCTL_VDD_UPDATE);
247 |
248 | if (quit) {
249 | break;
250 | }
251 | }
252 |
253 | pipe_thread_instance.join();
254 |
255 | // disconnect
256 | VddIoCtl(vdd, IOCTL_VDD_CONNECT);
257 | CloseHandle(vdd);
258 |
259 | return 0;
260 | }
--------------------------------------------------------------------------------
/一键关闭虚拟屏/.nomedia:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/一键关闭虚拟屏/STOP EasyVirtualDisplay.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | rem 关闭echo命令显示
3 |
4 | chcp 65001
5 | rem 强制开启UTF-8,以支持中文显示
6 |
7 | rem 为本脚本获取管理员权限
8 | ver | findstr "10\.[0-9]\.[0-9]*" >nul && goto powershellAdmin
9 |
10 | :mshtaAdmin
11 | rem 原理是利用mshta运行vbscript脚本给bat文件提权
12 | rem 这里使用了前后带引号的%~dpnx0来表示当前脚本,比原版的短文件名%~s0更可靠
13 | rem 这里使用了两次Net session,第二次是检测是否提权成功,如果提权失败则跳转到failed标签
14 | rem 这有效避免了提权失败之后bat文件继续执行的问题
15 | Net session >nul 2>&1 || mshta vbscript:CreateObject("Shell.Application").ShellExecute("cmd.exe","/c ""%~dpnx0""","","runas",1)(window.close)&&exit
16 | Net session >nul 2>&1 || goto failed
17 | goto Admin
18 |
19 | :powershellAdmin
20 | rem 原理是利用powershell给bat文件提权
21 | rem 这里使用了两次Net session,第二次是检测是否提权成功,如果提权失败则跳转到failed标签
22 | rem 这有效避免了提权失败之后bat文件继续执行的问题
23 | Net session >nul 2>&1 || powershell start-process \"%0\" -verb runas && exit
24 | Net session >nul 2>&1 || goto failed
25 | goto Admin
26 |
27 | :failed
28 | echo 提权失败,可能是杀毒软件拦截了提权操作,或者您没有同意UAC提权申请。
29 | echo 建议您右键点击此脚本,选择“以管理员身份运行”。
30 | echo 按任意键退出。
31 | pause >nul
32 | exit
33 |
34 | :Admin
35 | echo 本脚本处理所在路径:%0
36 | echo 已获取管理员权限!
37 | ping 127.0.0.1 -n 1 > nul
38 | echo 如果此窗口标题处显示“管理员”字样,那就说明提权成功了。
39 |
40 | echo 尝试将工作目录转移到软件目录...
41 | cd /d %~dp0
42 | rem 切换软件工作目录到软件所在文件夹
43 | ping 127.0.0.1 -n 1 > nul
44 | echo 目录转移成功!
45 |
46 | echo Designed by HPL at 2024年4月15日02点36分
47 | echo:
48 |
49 | set TITLE=STOP_EasyVirtualDisplay
50 | set executable=VirtualDisplayProject.exe
51 |
52 | title %TITLE%
53 |
54 | tasklist|findstr /i "%executable%"
55 | if errorlevel 1 (
56 | echo [%DATE%%TIME:~0,8%]未发现正在运行的%executable%!
57 | ping 127.0.0.1 -n 1 > nul
58 | msg %username% /time:6 "虚拟屏%executable%未开启"
59 | goto end
60 | )
61 | if errorlevel 0 (
62 | echo [%DATE%%TIME:~0,8%]已找到%executable%,将在1s后关闭%executable%!
63 | echo --------------------------------------------------------------------------------
64 | ping 127.0.0.1 -n 1 > nul
65 | goto stop
66 | )
67 |
68 | :stop
69 | taskkill /f /im %executable%
70 |
71 | msg %username% /time:6 "虚拟屏%executable%已关闭"
72 |
73 | :end
74 | exit
--------------------------------------------------------------------------------
/后台启动脚本/.nomedia:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/后台启动脚本/START MIN EasyVirtualDisplay.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | rem 关闭echo命令显示
3 |
4 | chcp 65001
5 | rem 强制开启UTF-8,以支持中文显示
6 |
7 | rem 为本脚本获取管理员权限
8 | ver | findstr "10\.[0-9]\.[0-9]*" >nul && goto powershellAdmin
9 |
10 | :mshtaAdmin
11 | rem 原理是利用mshta运行vbscript脚本给bat文件提权
12 | rem 这里使用了前后带引号的%~dpnx0来表示当前脚本,比原版的短文件名%~s0更可靠
13 | rem 这里使用了两次Net session,第二次是检测是否提权成功,如果提权失败则跳转到failed标签
14 | rem 这有效避免了提权失败之后bat文件继续执行的问题
15 | Net session >nul 2>&1 || mshta vbscript:CreateObject("Shell.Application").ShellExecute("cmd.exe","/c ""%~dpnx0""","","runas",1)(window.close)&&exit
16 | Net session >nul 2>&1 || goto failed
17 | goto Admin
18 |
19 | :powershellAdmin
20 | rem 原理是利用powershell给bat文件提权
21 | rem 这里使用了两次Net session,第二次是检测是否提权成功,如果提权失败则跳转到failed标签
22 | rem 这有效避免了提权失败之后bat文件继续执行的问题
23 | Net session >nul 2>&1 || powershell start-process \"%0\" -verb runas && exit
24 | Net session >nul 2>&1 || goto failed
25 | goto Admin
26 |
27 | :failed
28 | echo 提权失败,可能是杀毒软件拦截了提权操作,或者您没有同意UAC提权申请。
29 | echo 建议您右键点击此脚本,选择“以管理员身份运行”。
30 | echo 按任意键退出。
31 | pause >nul
32 | exit
33 |
34 | :Admin
35 | echo 本脚本处理所在路径:%0
36 | echo 已获取管理员权限!
37 | ping 127.0.0.1 -n 1 > nul
38 | echo 如果此窗口标题处显示“管理员”字样,那就说明提权成功了。
39 |
40 | echo 尝试将工作目录转移到软件目录...
41 | cd /d %~dp0
42 | rem 切换软件工作目录到软件所在文件夹
43 | ping 127.0.0.1 -n 1 > nul
44 | echo 目录转移成功!
45 |
46 | echo Designed by HPL at 2024年4月15日02点36分
47 | echo:
48 |
49 | set TITLE=START_MIN_EasyVirtualDisplay
50 | set executable=VirtualDisplayProject.exe
51 |
52 | title %TITLE%
53 |
54 | powershell -command "& {Start-Process %executable% -WindowStyle Hidden}"
55 |
56 | ping 127.0.0.1 -n 1 > nul
57 |
58 | echo [%DATE%%TIME:~0,8%]%executable% is running!
59 | echo:
60 | echo [%DATE%%TIME:~0,8%]%executable% is running! >> %TITLE%.log
61 | echo: >> %TITLE%.log
62 |
63 | ping 127.0.0.1 -n 1 > nul
64 |
65 | msg %username% /time:6 "已开启虚拟屏%executable%!"
66 |
67 | exit
--------------------------------------------------------------------------------