Skip to content

Commit

Permalink
这是一次大更新,包括:
Browse files Browse the repository at this point in the history
(1)根据【进程ID】,高亮指定窗体的任务条图标。
(2)给每个功能 js api 添加一个可选的【回调函数】参数,来日志 c-addon 模块内的工作细节。方便调试与发现问题。
  • Loading branch information
stuartZhang committed Aug 24, 2023
1 parent b6001f5 commit c8f5dfb
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 37 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "request-window-attention"
version = "0.1.2"
version = "0.1.3"
edition = "2021"
license = "MIT"
description = "在 windows 系统,根据窗体“标题名”闪烁窗体的任务栏图标来请求用户注意"
Expand Down
51 changes: 45 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,32 @@ struct GitEdition {
extern "C" {
/// 结束闪烁,但窗口任务栏还会继续高亮,直到窗体获得用户操作的焦点
/// @param winTitle 被闪烁窗体“标题名”
void stopFlashC(const char *win_title);
/// 开始闪烁。
void stopFlashByTitleC(const char *win_title);
/// /// 开始闪烁。
/// (1)在 stopFlashJs() 接口被调用后,闪烁会停止但高亮会继续。
/// (2)在窗体获得了焦点之后,闪烁与高亮才都会结束。
/// @param winTitle 被闪烁窗体“标题名”
/// @param blinkCount 闪烁次数。超过闪烁次数之后,任务栏会一直保持高亮状态。
/// @param blinkRate 相邻闪烁的间隔时间(单位:毫秒)
void startFlashC(const char *win_title, unsigned int count, unsigned int blink_rate);
void startFlashByTitleC(const char *win_title,
unsigned int count,
unsigned int blink_rate);
/// 结束闪烁,但窗口任务栏还会继续高亮,直到窗体获得用户操作的焦点
/// @param process_id 被闪烁窗体的进程ID或nwjs的进程PID
void stopFlashByPpidC(unsigned int process_id) ;
/// /// 开始闪烁。
/// (1)在 stopFlashJs() 接口被调用后,闪烁会停止但高亮会继续。
/// (2)在窗体获得了焦点之后,闪烁与高亮才都会结束。
/// @param process_id 被闪烁窗体的进程ID或nwjs的进程PID
/// @param blinkCount 闪烁次数。超过闪烁次数之后,任务栏会一直保持高亮状态。
/// @param blinkRate 相邻闪烁的间隔时间(单位:毫秒)
void startFlashByPpidC(unsigned int process_id,
unsigned int count,
unsigned int blink_rate);
/// 模块版本信息
GitEdition *getEditionC();
} // extern "C"

```
```typescript
Expand All @@ -76,20 +91,41 @@ export interface GitEdition {
pkgName: string;
pkgVersion: string;
}
export interface Logger {
(text: string): void;
}
/**
* 开始闪烁。
* (1)在 stopFlashJs() 接口被调用后,闪烁会停止但高亮会继续。
* (2)在窗体获得了焦点之后,闪烁与高亮才都会结束。
* @param winTitle 被闪烁窗体“标题名”
* @param blinkCount 闪烁次数。超过闪烁次数之后,任务栏会一直保持高亮状态。
* @param blinkRate 相邻闪烁的间隔时间(单位:毫秒)
* @param log 【可选】日志回调函数
*/
export function startFlashJs(winTitle: string, blinkCount: number, blinkRate: number);
export function startFlashByTitleJs(winTitle: string, blinkCount: number, blinkRate: number, log?: Logger);
/**
* 结束闪烁,但窗口任务栏还会继续高亮,直到窗体获得用户操作的焦点
* @param winTitle 被闪烁窗体“标题名”
* @param log 【可选】日志回调函数
*/
export function stopFlashByTitleJs(winTitle: string, log?: Logger);
/**
* 开始闪烁。
* (1)在 stopFlashJs() 接口被调用后,闪烁会停止但高亮会继续。
* (2)在窗体获得了焦点之后,闪烁与高亮才都会结束。
* @param ppid 被闪烁窗体的进程ID或nwjs的进程PID
* @param blinkCount 闪烁次数。超过闪烁次数之后,任务栏会一直保持高亮状态。
* @param blinkRate 相邻闪烁的间隔时间(单位:毫秒)
* @param log 【可选】日志回调函数
*/
export function stopFlashJs(winTitle: string);
export function startFlashByPpidJs(ppid: number, blinkCount: number, blinkRate: number, log?: Logger);
/**
* 结束闪烁,但窗口任务栏还会继续高亮,直到窗体获得用户操作的焦点
* @param ppid 被闪烁窗体的进程ID或nwjs的进程PID
* @param log 【可选】日志回调函数
*/
export function stopFlashByPpidJs(ppid: number, log?: Logger);
/**
* 模块版本信息
* @returns GitEdition
Expand All @@ -107,11 +143,14 @@ const attention = require('./dist/win-x64/request-window-attention.node');
(async () => {
console.info('版本信息', attention.getEdition());
// 通知操作系统,开始闪烁桌面任务栏图标
attention.startFlashJs('有道云笔记', 10, 500);
attention.startFlashJs('有道云笔记', 10, 500, logger);
await new Promise(resolve => setTimeout(resolve, 10000));
// 通知操作系统,停止闪烁桌面任务栏图标。但,任务栏图标还会继续高亮。
attention.stopFlashJs('有道云笔记');
})();
function logger(text){
console.log('[attention]', text);
}
```

另外,你也可以直接在工程根目录下运行指令`node test.js`来执行测试。
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "request-window-attention",
"version": "0.1.2",
"version": "0.1.3",
"description": "在 windows 系统,根据窗体“标题名”闪烁窗体的任务栏图标来请求用户注意",
"scripts": {
"prepublishOnly": "npm run build",
Expand Down
27 changes: 24 additions & 3 deletions request-window-attention.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,43 @@ export interface GitEdition {
pkgName: string;
pkgVersion: string;
}
export interface Logger {
(text: string): void;
}
/**
* 开始闪烁。
* (1)在 stopFlashJs() 接口被调用后,闪烁会停止但高亮会继续。
* (2)在窗体获得了焦点之后,闪烁与高亮才都会结束。
* @param winTitle 被闪烁窗体“标题名”
* @param blinkCount 闪烁次数。超过闪烁次数之后,任务栏会一直保持高亮状态。
* @param blinkRate 相邻闪烁的间隔时间(单位:毫秒)
* @param log 【可选】日志回调函数
*/
export function startFlashJs(winTitle: string, blinkCount: number, blinkRate: number);
export function startFlashByTitleJs(winTitle: string, blinkCount: number, blinkRate: number, log?: Logger);
/**
* 结束闪烁,但窗口任务栏还会继续高亮,直到窗体获得用户操作的焦点
* @param winTitle 被闪烁窗体“标题名”
* @param log 【可选】日志回调函数
*/
export function stopFlashByTitleJs(winTitle: string, log?: Logger);
/**
* 开始闪烁。
* (1)在 stopFlashJs() 接口被调用后,闪烁会停止但高亮会继续。
* (2)在窗体获得了焦点之后,闪烁与高亮才都会结束。
* @param ppid 被闪烁窗体的进程ID或nwjs的进程PID
* @param blinkCount 闪烁次数。超过闪烁次数之后,任务栏会一直保持高亮状态。
* @param blinkRate 相邻闪烁的间隔时间(单位:毫秒)
* @param log 【可选】日志回调函数
*/
export function startFlashByPpidJs(ppid: number, blinkCount: number, blinkRate: number, log?: Logger);
/**
* 结束闪烁,但窗口任务栏还会继续高亮,直到窗体获得用户操作的焦点
* @param ppid 被闪烁窗体的进程ID或nwjs的进程PID
* @param log 【可选】日志回调函数
*/
export function stopFlashJs(winTitle: string);
export function stopFlashByPpidJs(ppid: number, log?: Logger);
/**
* 模块版本信息
* @returns GitEdition
*/
export function getEdition(): GitEdition;
export function getEdition(): GitEdition;
52 changes: 52 additions & 0 deletions src/enum_windows.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use ::winapi::{shared::{minwindef::{BOOL, FALSE, LPARAM, TRUE}, windef::HWND}, um::winuser::{EnumWindows, EnumChildWindows, GetWindowThreadProcessId}};
use ::std::{ffi::c_void, mem};

pub fn enumerate_windows(process_id: u32, log: Option<&Box<dyn Fn(String)>>) -> Option<HWND> {
let mut result: Option<HWND> = None;
let mut callback = |hwnd1: HWND| -> bool {
let mut pid: u32 = 0;
unsafe { GetWindowThreadProcessId(hwnd1, &mut pid) };
#[cfg(debug_assertions)]
dbg!("enumerate_windows", hwnd1, pid);
log.map(|log| log(format!("[enumerate_windows] hwnd={hwnd1:?}; pid={pid}; process_id={process_id}")));
if process_id == pid {
result.replace(hwnd1);
false
} else if let Some(hwnd2) = enumerate_child_windows(hwnd1, process_id, log) {
result.replace(hwnd2);
false
} else {
true
}
};
let mut trait_obj: &mut dyn FnMut(HWND) -> bool = &mut callback;
let closure_pointer_pointer: *mut c_void = unsafe { mem::transmute(&mut trait_obj) };
let lparam = closure_pointer_pointer as LPARAM;
unsafe { EnumWindows(Some(enumerate_callback), lparam) };
result
}
pub fn enumerate_child_windows(hwnd1: HWND, process_id: u32, log: Option<&Box<dyn Fn(String)>>) -> Option<HWND> {
let mut result: Option<HWND> = None;
let mut callback = |hwnd2: HWND| -> bool {
let mut pid: u32 = 0;
unsafe { GetWindowThreadProcessId(hwnd2, &mut pid) };
#[cfg(debug_assertions)]
dbg!("enumerate_child_windows", hwnd2, pid);
log.map(|log| log(format!("[enumerate_child_windows] hwnd={hwnd2:?}; pid={pid}; process_id={process_id}")));
if process_id == pid {
result.replace(hwnd2);
false
} else {
true
}
};
let mut trait_obj: &mut dyn FnMut(HWND) -> bool = &mut callback;
let closure_pointer_pointer: *mut c_void = unsafe { mem::transmute(&mut trait_obj) };
let lparam = closure_pointer_pointer as LPARAM;
unsafe { EnumChildWindows(hwnd1, Some(enumerate_callback), lparam) };
result
}
unsafe extern "system" fn enumerate_callback(hwnd: HWND, lparam: LPARAM) -> BOOL {
let closure: &mut &mut dyn FnMut(HWND) -> bool = mem::transmute(lparam as *mut c_void);
if closure(hwnd) { TRUE } else { FALSE }
}
101 changes: 80 additions & 21 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,78 +1,137 @@
#![cfg_attr(debug_assertions, feature(trace_macros, log_syntax))]
mod git_edition;
mod enum_windows;
#[cfg(any(feature = "nodejs", feature = "nw"))]
use ::node_bindgen::{derive::node_bindgen, init::node_bindgen_init_once};
use ::std::{ffi::{c_char, c_uint, CString, OsString}, iter, mem, os::windows::ffi::OsStrExt};
use ::winapi::{_core::ptr::null_mut, um::winuser::{FindWindowW, FlashWindowEx, FLASHWINFO, FLASHW_CAPTION, FLASHW_TIMER, FLASHW_TIMERNOFG, FLASHW_STOP, FLASHW_TRAY}};
use ::node_bindgen::{core::{JSValue, NjError, val::{JsCallback, JsCallbackFunction, JsEnv, JsObject}}, derive::node_bindgen, init::node_bindgen_init_once, sys::{napi_callback_info, napi_value}};
use ::std::{cell::RefCell, ffi::{c_char, c_uint, CString, OsString}, iter, mem, os::windows::ffi::OsStrExt};
use ::winapi::{_core::ptr::null_mut, um::winuser::{FindWindowW, FlashWindowEx, FLASHWINFO, FLASHW_CAPTION, FLASHW_TIMER, FLASHW_TIMERNOFG, FLASHW_STOP, FLASHW_TRAY}, shared::windef::HWND};
pub use git_edition::GitEdition;
// ----------------------------------------------------------------------
// Rust
pub fn stop_flash(win_title: OsString){
flash(win_title, FLASHW_STOP, 0, 0)
// ----------------------------------------------------------------------
pub fn stop_flash_by_title(win_title: OsString, cb: Option<Box<dyn Fn(String)>>) {
flash_by_title(win_title, FLASHW_STOP, 0, 0, cb)
}
pub fn start_flash(win_title: OsString, count: u32, blink_rate: u32){
flash(win_title, FLASHW_CAPTION | FLASHW_TIMER | FLASHW_TIMERNOFG | FLASHW_TRAY, count, blink_rate)
pub fn start_flash_by_title(win_title: OsString, count: u32, blink_rate: u32, cb: Option<Box<dyn Fn(String)>>) {
flash_by_title(win_title, FLASHW_CAPTION | FLASHW_TIMER | FLASHW_TIMERNOFG | FLASHW_TRAY, count, blink_rate, cb)
}
fn flash(win_title: OsString, action: u32, count: u32, blink_rate: u32){
fn flash_by_title(win_title: OsString, action: u32, count: u32, blink_rate: u32, cb: Option<Box<dyn Fn(String)>>){
let winname = win_title.encode_wide().chain(iter::once(0)).collect::<Vec<u16>>();
let hwnd = unsafe { FindWindowW(null_mut(), winname.as_ptr()) };
if hwnd.is_null() {
return;
}
flash_only(hwnd, action, count, blink_rate, cb.as_ref());
}
//
pub fn stop_flash_by_ppid(process_id: u32, cb: Option<Box<dyn Fn(String)>>){
flash_by_ppid(process_id, FLASHW_STOP, 0, 0, cb)
}
pub fn start_flash_by_ppid(process_id: u32, count: u32, blink_rate: u32, cb: Option<Box<dyn Fn(String)>>) {
flash_by_ppid(process_id, FLASHW_CAPTION | FLASHW_TIMER | FLASHW_TIMERNOFG | FLASHW_TRAY, count, blink_rate, cb)
}
fn flash_by_ppid(process_id: u32, action: u32, count: u32, blink_rate: u32, cb: Option<Box<dyn Fn(String)>>) {
if let Some(hwnd) = enum_windows::enumerate_windows(process_id, cb.as_ref()) {
flash_only(hwnd, action, count, blink_rate, cb.as_ref());
}
}
//
fn flash_only(hwnd: HWND, action: u32, count: u32, blink_rate: u32, cb: Option<&Box<dyn Fn(String)>>) {
#[cfg(debug_assertions)]
dbg!(hwnd);
cb.map(|log| log(format!("[flash_only]hwnd={hwnd:?}; action={action}; count={count}; blink_rate={blink_rate}")));
let mut flash_info = FLASHWINFO {
cbSize: mem::size_of::<FLASHWINFO>() as u32,
hwnd,
dwFlags: action,
uCount: count,
dwTimeout: blink_rate
};
let _result = unsafe { FlashWindowEx(&mut flash_info) };
let result = unsafe { FlashWindowEx(&mut flash_info) };
#[cfg(debug_assertions)]
dbg!(_result);
dbg!(result);
cb.map(|log| log(format!("[flash_only]result={result:?};")));
}
// ----------------------------------------------------------------------
// C
// ----------------------------------------------------------------------
/// 结束闪烁,但窗口任务栏还会继续高亮,直到窗体获得用户操作的焦点
/// @param winTitle 被闪烁窗体“标题名”
#[export_name = "stopFlashC"]
pub extern fn stop_flash_c(win_title: *const c_char) {
#[export_name = "stopFlashByTitleC"]
pub extern fn stop_flash_by_title_c(win_title: *const c_char) {
let win_title = unsafe { CString::from_raw(win_title as *mut i8) }.into_string().unwrap();
stop_flash(win_title.into())
stop_flash_by_title(win_title.into(), None)
}
/// /// 开始闪烁。
/// (1)在 stopFlashJs() 接口被调用后,闪烁会停止但高亮会继续。
/// (2)在窗体获得了焦点之后,闪烁与高亮才都会结束。
/// @param winTitle 被闪烁窗体“标题名”
/// @param blinkCount 闪烁次数。超过闪烁次数之后,任务栏会一直保持高亮状态。
/// @param blinkRate 相邻闪烁的间隔时间(单位:毫秒)
#[export_name = "startFlashC"]
pub extern fn start_flash_c(win_title: *const c_char, count: c_uint, blink_rate: c_uint) {
#[export_name = "startFlashByTitleC"]
pub extern fn start_flash_by_title_c(win_title: *const c_char, count: c_uint, blink_rate: c_uint) {
let win_title = unsafe { CString::from_raw(win_title as *mut i8) }.into_string().unwrap();
start_flash(win_title.into(), count, blink_rate)
start_flash_by_title(win_title.into(), count, blink_rate, None)
}
/// 结束闪烁,但窗口任务栏还会继续高亮,直到窗体获得用户操作的焦点
/// @param process_id 被闪烁窗体的进程ID或nwjs的进程PID
#[export_name = "stopFlashByPpidC"]
pub extern fn stop_flash_by_ppid_c(process_id: c_uint) {
stop_flash_by_ppid(process_id, None)
}
/// /// 开始闪烁。
/// (1)在 stopFlashJs() 接口被调用后,闪烁会停止但高亮会继续。
/// (2)在窗体获得了焦点之后,闪烁与高亮才都会结束。
/// @param process_id 被闪烁窗体的进程ID或nwjs的进程PID
/// @param blinkCount 闪烁次数。超过闪烁次数之后,任务栏会一直保持高亮状态。
/// @param blinkRate 相邻闪烁的间隔时间(单位:毫秒)
#[export_name = "startFlashByPpidC"]
pub extern fn start_flash_by_ppid_c(process_id: c_uint, count: c_uint, blink_rate: c_uint) {
start_flash_by_ppid(process_id, count, blink_rate, None)
}
/// 模块版本信息
#[export_name = "getEditionC"]
pub extern fn get_edition_c() -> *mut GitEdition {
Box::into_raw(Box::new(GitEdition::default()))
}
// ----------------------------------------------------------------------
// nodejs
// ----------------------------------------------------------------------
#[cfg_attr(any(feature = "nodejs", feature = "nw"), node_bindgen_init_once)]
#[cfg(any(feature = "nodejs", feature = "nw"))]
fn main() {
println!("{}", GitEdition::default());
}
#[cfg_attr(any(feature = "nodejs", feature = "nw"), node_bindgen)]
#[cfg(any(feature = "nodejs", feature = "nw"))]
fn stop_flash_js(win_title: String) {
stop_flash(win_title.into())
fn stop_flash_by_title_js(win_title: String, cb: Option<JsCallbackFunction>) {
stop_flash_by_title(win_title.into(), build_callback(cb))
}
#[cfg_attr(any(feature = "nodejs", feature = "nw"), node_bindgen)]
#[cfg(any(feature = "nodejs", feature = "nw"))]
fn start_flash_by_title_js(win_title: String, count: u32, blink_rate: u32, cb: Option<JsCallbackFunction>) {
start_flash_by_title(win_title.into(), count, blink_rate, build_callback(cb))
}
#[cfg_attr(any(feature = "nodejs", feature = "nw"), node_bindgen)]
#[cfg(any(feature = "nodejs", feature = "nw"))]
fn start_flash_js(win_title: String, count: u32, blink_rate: u32) {
start_flash(win_title.into(), count, blink_rate)
fn stop_flash_by_ppid_js(process_id: i32, cb: Option<JsCallbackFunction>) {
stop_flash_by_ppid(process_id as u32, build_callback(cb))
}
#[cfg_attr(any(feature = "nodejs", feature = "nw"), node_bindgen(name = "getEdition"))]
#[cfg_attr(any(feature = "nodejs", feature = "nw"), node_bindgen)]
#[cfg(any(feature = "nodejs", feature = "nw"))]
fn start_flash_by_ppid_js(process_id: i32, count: u32, blink_rate: u32, cb: Option<JsCallbackFunction>) {
start_flash_by_ppid(process_id as u32, count, blink_rate, build_callback(cb))
}
#[cfg_attr(any(feature = "nodejs", feature = "nw"), node_bindgen)]
#[cfg(any(feature = "nodejs", feature = "nw"))]
fn get_edition() -> GitEdition {
GitEdition::default()
}
#[cfg(any(feature = "nodejs", feature = "nw"))]
fn build_callback(cb: Option<JsCallbackFunction>) -> Option<Box<dyn Fn(String)>> {
cb.map(|cb| {
Box::new(move |text: String| {
cb.call(vec![text]).unwrap();
}) as Box<dyn Fn(String)>
})
}
8 changes: 6 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use ::std::{thread, time::Duration};
fn main() {
request_window_attention::start_flash("有道云笔记".into(), 10, 500);
request_window_attention::start_flash_by_title("有道云笔记".into(), 10, 500, None);
thread::sleep(Duration::from_secs(2));
request_window_attention::stop_flash("有道云笔记".into());
request_window_attention::stop_flash_by_title("有道云笔记".into(), None);
//
request_window_attention::start_flash_by_ppid(3300, 10, 500, None);
thread::sleep(Duration::from_secs(2));
request_window_attention::stop_flash_by_ppid(3300, None);
}
Loading

0 comments on commit c8f5dfb

Please sign in to comment.