├── .gitignore ├── README.md ├── Cargo.toml ├── LICENSE ├── Cargo.lock └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Taskmgr Drawing 2 | 3 | ## Usage 4 | 5 | 1. Download [ffplay.exe](https://ffmpeg.org/download.html#build-windows) and add it to `PATH` 6 | 2. Download a video 7 | 3. Edit title, color and window margin values in the code 8 | 4. Open an elevated command prompt 9 | 5. Run `cargo run -- your_video.mp4` 10 | 11 | ## License 12 | 13 | MIT License 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "taskmgr-drawing" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.58" 10 | 11 | [dependencies.windows] 12 | version = "0.38.0" 13 | features = [ 14 | "alloc", 15 | "Win32_Foundation", 16 | "Win32_Graphics_Gdi", 17 | "Win32_UI_HiDpi", 18 | "Win32_UI_WindowsAndMessaging", 19 | ] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ganlv 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.58" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" 10 | 11 | [[package]] 12 | name = "taskmgr-drawing" 13 | version = "0.1.0" 14 | dependencies = [ 15 | "anyhow", 16 | "windows", 17 | ] 18 | 19 | [[package]] 20 | name = "windows" 21 | version = "0.38.0" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | checksum = "0c47017195a790490df51a3e27f669a7d4f285920d90d03ef970c5d886ef0af1" 24 | dependencies = [ 25 | "windows_aarch64_msvc", 26 | "windows_i686_gnu", 27 | "windows_i686_msvc", 28 | "windows_x86_64_gnu", 29 | "windows_x86_64_msvc", 30 | ] 31 | 32 | [[package]] 33 | name = "windows_aarch64_msvc" 34 | version = "0.38.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "b12add87e2fb192fff3f4f7e4342b3694785d79f3a64e2c20d5ceb5ccbcfc3cd" 37 | 38 | [[package]] 39 | name = "windows_i686_gnu" 40 | version = "0.38.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "4c98f2db372c23965c5e0f43896a8f0316dc0fbe48d1aa65bea9bdd295d43c15" 43 | 44 | [[package]] 45 | name = "windows_i686_msvc" 46 | version = "0.38.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "cdf0569be0f2863ab6a12a6ba841fcfa7d107cbc7545a3ebd57685330db0a3ff" 49 | 50 | [[package]] 51 | name = "windows_x86_64_gnu" 52 | version = "0.38.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "905858262c8380a36f32cb8c1990d7e7c3b7a8170e58ed9a98ca6d940b7ea9f1" 55 | 56 | [[package]] 57 | name = "windows_x86_64_msvc" 58 | version = "0.38.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "890c3c6341d441ffb38f705f47196e3665dc6dd79f6d72fa185d937326730561" 61 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::mem::size_of; 2 | use std::process::Command; 3 | use std::ptr::null_mut; 4 | use std::thread::sleep; 5 | use std::time::Duration; 6 | use anyhow::anyhow; 7 | use windows::Win32::Foundation::RECT; 8 | use windows::Win32::Graphics::Gdi::{BI_BITFIELDS, BitBlt, BITMAPINFO, BITMAPINFOHEADER, CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject, DIB_RGB_COLORS, GetDC, GetDIBits, ReleaseDC, SelectObject, SetDIBits, SRCCOPY}; 9 | use windows::Win32::UI::HiDpi::{PROCESS_PER_MONITOR_DPI_AWARE, SetProcessDpiAwareness}; 10 | use windows::Win32::UI::WindowsAndMessaging::{FindWindowW, GetClientRect, SetWindowPos, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOZORDER}; 11 | 12 | pub struct Image { 13 | pub width: usize, 14 | pub height: usize, 15 | pub buf: Vec, 16 | } 17 | 18 | impl Image { 19 | pub fn new(width: usize, height: usize) -> Self { 20 | Self { 21 | width, 22 | height, 23 | buf: vec![0u8; width * height * 4], 24 | } 25 | } 26 | pub fn from_window(class_name: &str, window_name: &str, x: i32, y: i32) -> anyhow::Result { 27 | unsafe { 28 | let hwnd = FindWindowW(class_name, window_name); 29 | if hwnd.0 == 0 { 30 | return Err(anyhow!("Window not found")); 31 | } 32 | let mut rect = RECT::default(); 33 | let _ = GetClientRect(hwnd, &mut rect); 34 | let width = (rect.right - rect.left) as usize; 35 | let height = (rect.bottom - rect.top) as usize; 36 | if width == 0 || height == 0 { 37 | return Err(anyhow!("Window not shown")); 38 | } 39 | let hdc = GetDC(hwnd); 40 | let hmemdc = CreateCompatibleDC(hdc); 41 | let hbm = CreateCompatibleBitmap(hdc, width as i32, height as i32); 42 | let hbm_old = SelectObject(hmemdc, hbm); 43 | BitBlt(hmemdc, 0, 0, width as i32, height as i32, hdc, x, y, SRCCOPY); 44 | let mut bmi_buf = [0u8; (size_of::() + 8)]; // 因为调色板 bmiColors 是个变长数组,RGB 三个颜色,数组实际长度是 3,比 1 个元素多出 8 字节 45 | let bmi = &mut *(bmi_buf.as_mut_ptr() as *mut BITMAPINFO); 46 | bmi.bmiHeader.biSize = size_of::() as u32; 47 | GetDIBits(hmemdc, hbm, 0, 0, null_mut(), bmi, DIB_RGB_COLORS); 48 | bmi.bmiHeader.biBitCount = 32; 49 | bmi.bmiHeader.biCompression = BI_BITFIELDS as u32; 50 | bmi.bmiColors.get_unchecked_mut(0).rgbRed = 255; 51 | bmi.bmiColors.get_unchecked_mut(1).rgbGreen = 255; 52 | bmi.bmiColors.get_unchecked_mut(2).rgbBlue = 255; 53 | let mut buf = vec![0u8; width * height * 4]; 54 | GetDIBits(hmemdc, hbm, 0, height as u32, buf.as_mut_ptr() as _, bmi, DIB_RGB_COLORS); 55 | let _ = SelectObject(hmemdc, hbm_old); 56 | DeleteObject(hbm); 57 | DeleteDC(hmemdc); 58 | ReleaseDC(hwnd, hdc); 59 | Ok(Self { 60 | width, 61 | height, 62 | buf, 63 | }) 64 | } 65 | } 66 | pub fn paint_to_window(&self, class_name: &str, window_name: &str, x: i32, y: i32) -> anyhow::Result<()> { 67 | unsafe { 68 | let hwnd = FindWindowW(class_name, window_name); 69 | if hwnd.0 == 0 { 70 | return Err(anyhow!("Window not found")); 71 | } 72 | let hdc = GetDC(hwnd); 73 | let hmemdc = CreateCompatibleDC(hdc); 74 | let hbm = CreateCompatibleBitmap(hdc, self.width as i32, self.height as i32); 75 | let hbm_old = SelectObject(hmemdc, hbm); 76 | let mut bmi_buf = [0u8; (size_of::() + 8)]; 77 | let bmi = &mut *(bmi_buf.as_mut_ptr() as *mut BITMAPINFO); 78 | bmi.bmiHeader.biSize = size_of::() as u32; 79 | GetDIBits(hmemdc, hbm, 0, 0, null_mut(), bmi, DIB_RGB_COLORS); 80 | bmi.bmiHeader.biBitCount = 32; 81 | bmi.bmiHeader.biCompression = BI_BITFIELDS as u32; 82 | bmi.bmiColors.get_unchecked_mut(0).rgbRed = 255; 83 | bmi.bmiColors.get_unchecked_mut(1).rgbGreen = 255; 84 | bmi.bmiColors.get_unchecked_mut(2).rgbBlue = 255; 85 | SetDIBits(hmemdc, hbm, 0, self.height as u32, self.buf.as_ptr() as _, bmi, DIB_RGB_COLORS); 86 | BitBlt(hdc, x, y, self.width as i32, self.height as i32, hmemdc, 0, 0, SRCCOPY); 87 | let _ = SelectObject(hmemdc, hbm_old); 88 | DeleteObject(hbm); 89 | DeleteDC(hmemdc); 90 | ReleaseDC(hwnd, hdc); 91 | Ok(()) 92 | } 93 | } 94 | pub fn from_fn (u8, u8, u8)>(width: usize, height: usize, f: F) -> Self { 95 | let mut image = Self::new(width, height); 96 | for y in 0..height { 97 | for x in 0..width { 98 | let (r, g, b) = f(x, y); 99 | image.set_color(x, y, r, g, b); 100 | } 101 | } 102 | image 103 | } 104 | pub fn get_offset(&self, x: usize, y: usize) -> usize { 105 | ((self.height - 1 - y) * self.width + x) * 4 106 | } 107 | pub fn get_color(&self, x: usize, y: usize) -> (u8, u8, u8) { 108 | let offset = self.get_offset(x, y); 109 | (self.buf[offset + 2], self.buf[offset + 1], self.buf[offset + 0]) 110 | } 111 | pub fn set_color(&mut self, x: usize, y: usize, r: u8, g: u8, b: u8) { 112 | let offset = self.get_offset(x, y); 113 | self.buf[offset + 2] = r; 114 | self.buf[offset + 1] = g; 115 | self.buf[offset + 0] = b; 116 | } 117 | pub fn get_grayscale_color(&self, x: usize, y: usize) -> u8 { 118 | let (r, g, b) = self.get_color(x, y); 119 | return r / 4 + g / 2 + b / 4; 120 | } 121 | pub fn is_white(&self, x: usize, y: usize) -> bool { 122 | self.get_grayscale_color(x, y) > 192 123 | } 124 | pub fn is_edge(&self, x: usize, y: usize) -> bool { 125 | let a = self.is_white(x, y); 126 | let b = self.is_white(x, y + 1); 127 | let c = self.is_white(x + 1, y); 128 | let d = self.is_white(x + 1, y + 1); 129 | !(a == b && a == c && a == d) 130 | } 131 | pub fn to_taskmgr_style(&self) -> Self { 132 | Self::from_fn(self.width - 1, self.height - 1, |x, y| { 133 | if x == 0 || y == 0 || x == self.width - 2 || y == self.height - 2 || self.is_edge(x, y) { 134 | (0x4c, 0x9d, 0xcb) // 边框 135 | } else if x % 50 == 0 || y % 50 == 0 { 136 | (0xd9, 0xea, 0xf4) // 网格 137 | } else if self.is_white(x, y) { 138 | (0xff, 0xff, 0xff) // 白色 139 | } else { 140 | (0xf1, 0xf6, 0xfa) // 黑色 141 | } 142 | }) 143 | } 144 | } 145 | 146 | fn main() -> anyhow::Result<()> { 147 | unsafe { 148 | let _ = SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); 149 | 150 | let hwnd_taskmgr = FindWindowW("TaskManagerWindow", "任务管理器"); 151 | if hwnd_taskmgr.0 == 0 { 152 | Command::new("taskmgr.exe") 153 | .spawn()?; 154 | } 155 | let hwnd_ffplay = FindWindowW("SDL_app", "ffplay"); 156 | if hwnd_ffplay.0 == 0 { 157 | Command::new("ffplay.exe") 158 | .arg("-x") 159 | .arg("540") 160 | .arg("-volume") 161 | .arg("1") 162 | .arg("-window_title") 163 | .arg("ffplay") 164 | .arg(std::env::args().skip(1).next().unwrap()) 165 | .spawn()?; 166 | } 167 | 168 | loop { 169 | if let Ok(img) = Image::from_window("SDL_app", "ffplay", 0, 0) { 170 | let hwnd_taskmgr = FindWindowW("TaskManagerWindow", "任务管理器"); 171 | if hwnd_taskmgr.0 != 0 { 172 | let img2 = img.to_taskmgr_style(); 173 | SetWindowPos(hwnd_taskmgr, None, 0, 0, (img2.width + 396) as i32, (img2.height + 508) as i32, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); 174 | let _ = img2.paint_to_window("TaskManagerWindow", "任务管理器", 350, 126); 175 | } else { 176 | sleep(Duration::from_millis(16)); 177 | } 178 | } else { 179 | sleep(Duration::from_millis(16)); 180 | } 181 | } 182 | } 183 | } --------------------------------------------------------------------------------