音ではなく視覚で知らせるタイマー
もちろん視覚障害が起こらない程度の刺激に抑えながら 時間になると点滅するwindowが出てくる
(フラッシュのような動画になってしまったのでリンクのみにした)
code:Cargo.toml
name = "visual_alert"
version = "0.1.0"
edition = "2021"
windows = { version = "0.52.0", features = [
"Win32_Foundation",
"Win32_System_Com",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
"Win32_Graphics_Gdi",
"Win32_System_LibraryLoader",
]}
code:src/main.rs
use std::{
env, process, thread,
time::{Duration, Instant},
};
use windows::{
core::w,
core::*,
Win32::{
Foundation::*,
Graphics::Gdi::*,
System::{Com::*, LibraryLoader::GetModuleHandleW},
UI::WindowsAndMessaging::*,
},
};
// ウィンドウの状態を管理するためのグローバルな変数(安全な範囲で使用)
static mut IS_ALERT_STATE: bool = true;
fn main() -> Result<()> {
// 1. コマンドライン引数から時間をパース
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("エラー: 時間を指定してください。");
println!("使用例:");
println!(" visual_alert.exe 5m30s -> 5分30秒");
println!(" visual_alert.exe 120s -> 2分");
println!(" visual_alert.exe 10m -> 10分");
println!(" visual_alert.exe 120 -> 120分");
process::exit(1);
}
let duration = match parse_duration_arg(&args1) { Ok(d) if d.as_secs() > 0 => d,
Ok(_) => {
println!("エラー: 0秒は指定できません。1秒以上を指定してください。");
process::exit(1);
}
Err(e) => {
println!("エラー: {}", e);
println!("サポートされる表記例: 5m30s, 120s, 10m, 120");
process::exit(1);
}
};
// 2. タイマーを開始
let total = duration.as_secs();
let mins = total / 60;
let secs = total % 60;
if secs == 0 {
println!("{}分のタイマーを開始します...", mins);
} else if mins == 0 {
println!("{}秒のタイマーを開始します...", secs);
} else {
println!("{}分{}秒のタイマーを開始します...", mins, secs);
}
run_countdown(duration);
println!("\n時間になりました。アラートウィンドウを表示します。");
// 3. ウィンドウを作成し、メッセージループを開始
unsafe {
// COMライブラリを初期化
CoInitialize(None)?;
let instance = GetModuleHandleW(None)?;
let window_class_name = w!("VisualAlertWindowClass");
// ウィンドウクラスの登録
let wc = WNDCLASSW {
hCursor: LoadCursorW(None, IDC_ARROW)?,
// GetModuleHandleW は HMODULE を返すため、HINSTANCE に明示変換
hInstance: HINSTANCE(instance.0),
lpszClassName: window_class_name,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(wndproc),
..Default::default()
};
RegisterClassW(&wc);
// 画面中央に表示するための座標計算
let screen_width = GetSystemMetrics(SM_CXSCREEN);
let screen_height = GetSystemMetrics(SM_CYSCREEN);
let window_width = 500;
let window_height = 300;
let x = (screen_width - window_width) / 2;
let y = (screen_height - window_height) / 2;
// ウィンドウの作成
let hwnd = CreateWindowExW(
WS_EX_TOPMOST, // 常に最前面に表示
window_class_name,
w!("時間です!"),
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU, // シンプルなウィンドウスタイル
x,
y,
window_width,
window_height,
None,
None,
instance,
None,
);
// 仮想デスクトップへのピン留めは API 仕様差異がありコンパイルエラーとなるため削除
// 必要であれば IVirtualDesktopPinnedApps を用いた実装に置き換えてください。
ShowWindow(hwnd, SW_SHOW);
// 0.5秒ごとに点滅させるためのタイマーを設定
SetTimer(hwnd, 1, 500, None);
// メッセージループ
let mut message = MSG::default();
while GetMessageW(&mut message, None, 0, 0).into() {
TranslateMessage(&message);
DispatchMessageW(&message);
}
}
Ok(())
}
// ターミナルでカウントダウンを表示する関数
fn run_countdown(duration: Duration) {
let start = Instant::now();
while start.elapsed() < duration {
let remaining = duration - start.elapsed();
let mins = remaining.as_secs() / 60;
let secs = remaining.as_secs() % 60;
print!("\r残り時間: {:02}:{:02} ", mins, secs);
thread::sleep(Duration::from_millis(100));
}
}
// 引数の時間文字列を Duration にパースする
// サポート: "5m30s", "120s", "10m", "120"(分)
fn parse_duration_arg(input: &str) -> std::result::Result<Duration, String> {
let s = input.trim();
if s.is_empty() {
return Err("空の文字列です".into());
}
// 数字のみ -> 分として解釈
if s.chars().all(|c| c.is_ascii_digit()) {
let minutes: u64 = s
.parse()
.map_err(|_| "数値のパースに失敗しました".to_string())?;
return Ok(Duration::from_secs(minutes.saturating_mul(60)));
}
// m/s 付きのトークンを順に読む(順不同許可)
let mut minutes: u128 = 0;
let mut seconds: u128 = 0;
let mut num: u128 = 0;
let mut has_token = false;
for ch in s.chars() {
if ch.is_ascii_digit() {
has_token = true;
num = num
.checked_mul(10)
.and_then(|v| v.checked_add((ch as u8 - b'0') as u128))
.ok_or_else(|| "数値が大きすぎます".to_string())?;
} else if ch == 'm' || ch == 'M' {
minutes = minutes
.checked_add(num)
.ok_or_else(|| "分の値が大きすぎます".to_string())?;
num = 0;
} else if ch == 's' || ch == 'S' {
seconds = seconds
.checked_add(num)
.ok_or_else(|| "秒の値が大きすぎます".to_string())?;
num = 0;
} else {
return Err(format!("不明な文字 '{}' が含まれています", ch));
}
}
// 末尾が数字で終わった場合はエラー(単位が必要)
if num != 0 {
return Err("末尾の数値に単位がありません。m または s を付けてください".into());
}
if !has_token {
return Err("有効な時間指定が見つかりません".into());
}
// 正規化(秒>=60もそのまま合算)
let total_seconds = minutes
.checked_mul(60)
.and_then(|v| v.checked_add(seconds))
.ok_or_else(|| "合計秒が大きすぎます".to_string())?;
let secs_u64 = u64::try_from(total_seconds).map_err(|_| "合計秒が大きすぎます".to_string())?;
Ok(Duration::from_secs(secs_u64))
}
// ウィンドウプロシージャ(ウィンドウのイベントを処理する関数)
extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
unsafe {
match message {
// 描画イベント
WM_PAINT => {
let mut ps = PAINTSTRUCT::default();
let hdc = BeginPaint(window, &mut ps);
// 点滅状態に応じて背景色と文字色を決定
let (bg_color, text_color) = if IS_ALERT_STATE {
(COLORREF(0x002626DC), COLORREF(0x00FFFFFF)) // Red, White
} else {
(COLORREF(0x00F6F4F3), COLORREF(0x00000000)) // White-ish, Black
};
let brush = CreateSolidBrush(bg_color);
FillRect(hdc, &ps.rcPaint, brush);
DeleteObject(brush);
SetTextColor(hdc, text_color);
SetBkMode(hdc, TRANSPARENT);
let mut rect = ps.rcPaint;
// DrawTextW (4 引数版) は &mut u16 (PWSTR) を受け取り、ヌル終端で長さを判定する let text_pc = w!("時間です!");
let mut text_buf: Vec<u16> = text_pc.as_wide().to_vec();
DrawTextW(
hdc,
text_buf.as_mut_slice(),
&mut rect,
DT_CENTER | DT_VCENTER | DT_SINGLELINE,
);
EndPaint(window, &ps);
LRESULT(0)
}
// タイマーイベント
WM_TIMER => {
// 点滅状態を反転させて再描画を要求
IS_ALERT_STATE = !IS_ALERT_STATE;
InvalidateRect(window, None, true);
LRESULT(0)
}
// ウィンドウが閉じられるときのイベント
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(window, message, wparam, lparam),
}
}
}
やりたいこと
Rustにもっと寄せた書き方をする
crate attributeでそういうのがあるみたい