ファイル名に日時を入れています。
今日なら、251103ファイル名
プログラムで自動的にクリップボードへ入れてペーストして使っていた。
そのプログラムを強化した。
いちいち、プログラム起動ボタンを押すのがへきへきしてきたため。
これまでは、起動と同時に、6桁文字列作成、クリップボードへ貼り付け、プログラム終了としていた。
ChatCPTの力を借り、以下のコードを作成した。
常駐プログラムとして起動。
右クリックを5回でクリップボードにペースト。
もしくは、右ドラッグ
〇
|
でペースト
とした。
// DateGestureClipper_Tray.cpp
// Win11 + MSVC 2022 /SUBSYSTEM:WINDOWS 1ファイル完結(コンソール非表示)
//
// 目的:
// - 現在の日付「yyMMdd」をクリップボードへコピー(例:251103)
// - 常駐監視:右クリック5連打、または右ドラッグの円ジェスチャで発火
// - 無音フィードバック:半透明の小さなOSDを一瞬だけ表示(約0.9秒)
// - タスクトレイ(通知領域)に常駐アイコンを出し、以下のメニューを提供:
// * 「YYMMDD」 → 即コピー
// * 「バージョン情報」→ “mk 2025 ChatGPT” を表示
// * 「終了」 → 安全に終了
// - トレイアイコン左クリックでも即コピー
//
// 使い方:
// - Windowsアプリケーション(/SUBSYSTEM:WINDOWS)で新規プロジェクトを作り、
// このファイルを追加してビルド。生成EXEを起動すると非表示で常駐します。
// - スタートアップ登録は Win+R → shell:startup にショートカットを置くと便利。
// - 開発中・運用中に終了したい場合は、タスクトレイのアイコンを右クリック→「終了」。
// (左クリックは即コピー、右クリックはメニュー)
//
// 重要設計メモ:
// - 低レベルマウスフック (WH_MOUSE_LL) はプロセス境界を越えてグローバルに動くため、
// フック解除・終了時の後始末(UnhookWindowsHookEx)が重要。本コードはメイン隠しウィンドウの
// ライフサイクルに紐づけて安全に管理しています。
// - OSDはウィンドウプロシージャ(同一スレッド)内のタイマーで自壊させ、他スレッドDestroy問題を回避。
// - トレイアイコンは NOTIFYICONDATA(uCallbackMessage) を使って独自メッセージ (WM_APP+1) を受け取り、
// 左クリック/右クリックの振る舞いを実装。
// - 角度正規化は <numbers> の std::numbers::pi_v<double> を使用(M_PI 非依存)。
// - すべての可視UIは「最前面・非アクティブ・透過」の小窓のみ(フォーカスを奪わない)。
//
#include <windows.h>
#include <shellapi.h>
#include <string>
#include <vector>
#include <atomic>
#include <numbers> // std::numbers::pi_v<double>
#include <cmath>
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdi32.lib")
#pragma comment(lib, "shell32.lib")
//========================= 調整用パラメータ =========================//
// 5連打(右クリック)関連
constexpr DWORD TAP_WINDOW_MS = 800; // 5回をこの時間内に
constexpr int TAP_COUNT_TARGET = 5; // 目標クリック回数
constexpr DWORD FIRE_GUARD_MS = 300; // 誤連発ガード間隔(ms)
// 円ジェスチャ関連(右ボタン押下中に円を描いたら発火)
constexpr double MIN_PATH_LEN = 200.0; // 総軌跡長がこれ未満なら無視
constexpr double MIN_SWEEP_RAD = 4.5; // 累積回転角の下限 (rad) ≒ 260°
constexpr double MAX_REL_RADIUS_STD = 0.35; // 半径の相対標準偏差(円らしさ)
constexpr int MIN_POINTS = 30; // 判定に必要な最低点数
// OSD(無音ポップアップ)関連
constexpr UINT OSD_LIFETIME_MS = 900; // 表示時間
constexpr int OSD_W = 240; // 幅
constexpr int OSD_H = 48; // 高さ
constexpr BYTE OSD_ALPHA = 230; // 不透明度(0-255)
// トレイアイコン関連
constexpr UINT WMAPP_TRAYICON = WM_APP + 1; // コールバック用メッセージ
constexpr UINT ID_TRAY_COPY = 1001; // メニュー:YYMMDD
constexpr UINT ID_TRAY_ABOUT = 1002; // メニュー:バージョン情報
constexpr UINT ID_TRAY_EXIT = 1003; // メニュー:終了
// 隠しメインウィンドウのクラス名
static const wchar_t* kMainWndClass = L"DateGestureClipperHiddenMainWnd";
// OSD小窓のクラス名
static const wchar_t* kOSDClass = L"DateClipOSDWndClass";
//========================= グローバル状態 =========================//
// (注)最低限に留める。終了時の掃除を確実に。
HHOOK g_hMouseLL = nullptr; // 低レベルマウスフック
NOTIFYICONDATAW g_nid{}; // トレイアイコン情報
std::atomic<DWORD> g_lastFire{ 0 }; // 連発ガード用タイムスタンプ
// 右クリック5連打の状態
DWORD g_firstTapTick = 0;
int g_tapCount = 0;
// 円ジェスチャ状態
std::vector<POINT> g_path; // 軌跡点列
bool g_rbtnDown = false; // 右ボタン押下フラグ
//========================= ユーティリティ =========================//
// yyMMdd を作成(例:251103)
std::wstring MakeYYMMDD() {
SYSTEMTIME st{}; GetLocalTime(&st);
wchar_t buf[16];
swprintf_s(buf, L"%02d%02d%02d", st.wYear % 100, st.wMonth, st.wDay);
return buf;
}
// 文字列をクリップボード(Unicode)へ
bool CopyToClipboard(const std::wstring& w) {
if (!OpenClipboard(nullptr)) return false;
EmptyClipboard();
const SIZE_T bytes = (w.size() + 1) * sizeof(wchar_t);
HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, bytes);
if (!h) { CloseClipboard(); return false; }
void* p = GlobalLock(h);
if (!p) { GlobalFree(h); CloseClipboard(); return false; }
memcpy(p, w.c_str(), bytes);
GlobalUnlock(h);
SetClipboardData(CF_UNICODETEXT, h); // 以後はシステム管理
CloseClipboard();
return true;
}
// 簡易距離・角度
inline double Dist(double x1, double y1, double x2, double y2) {
const double dx = x2 - x1, dy = y2 - y1;
return std::sqrt(dx * dx + dy * dy);
}
inline double Angle(double x1, double y1, double x2, double y2) {
return std::atan2(y2 - y1, x2 - x1);
}
//========================= OSD(半透明無音フィードバック) =========================//
// OSDは「自分のWndProcタイマーで自壊」させる(他スレDestroyによる消滅不具合を避ける)
static LRESULT CALLBACK OSDWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_CREATE:
SetTimer(hWnd, 1, OSD_LIFETIME_MS, nullptr); // 寿命タイマー
return 0;
case WM_TIMER:
if (wParam == 1) {
KillTimer(hWnd, 1);
DestroyWindow(hWnd); // 自スレッドで安全に破棄
}
return 0;
case WM_PAINT: {
PAINTSTRUCT ps;
HDC dc = BeginPaint(hWnd, &ps);
RECT rc; GetClientRect(hWnd, &rc);
// 背景(白系)+枠線
HBRUSH br = CreateSolidBrush(RGB(250, 250, 250));
FillRect(dc, &rc, br);
DeleteObject(br);
FrameRect(dc, &rc, (HBRUSH)GetStockObject(GRAY_BRUSH));
// テキスト
SetBkMode(dc, TRANSPARENT);
DrawTextW(dc, L"コピーしました", -1, &rc,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
EndPaint(hWnd, &ps);
return 0;
}
default:
return DefWindowProcW(hWnd, msg, wParam, lParam);
}
}
// 画面右上あたりに小窓を0.9秒だけ出す(フォーカスは奪わない)
void ShowOSDQuick() {
static ATOM a = 0;
if (!a) {
WNDCLASSW wc{};
wc.lpfnWndProc = OSDWndProc;
wc.hInstance = GetModuleHandleW(nullptr);
wc.lpszClassName = kOSDClass;
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
a = RegisterClassW(&wc);
}
const int x = GetSystemMetrics(SM_CXSCREEN) - (OSD_W + 20);
const int y = 40;
HWND wnd = CreateWindowExW(
WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_NOACTIVATE,
kOSDClass, L"", WS_POPUP,
x, y, OSD_W, OSD_H,
nullptr, nullptr, GetModuleHandleW(nullptr), nullptr);
SetLayeredWindowAttributes(wnd, 0, OSD_ALPHA, LWA_ALPHA);
ShowWindow(wnd, SW_SHOWNA);
UpdateWindow(wnd);
}
//========================= コア動作(コピー+無音通知) =========================//
void FireCopy() {
const DWORD now = GetTickCount();
if (now - g_lastFire.load() < FIRE_GUARD_MS) return; // 連発ガード
g_lastFire.store(now);
const std::wstring s = MakeYYMMDD();
if (CopyToClipboard(s)) {
ShowOSDQuick(); // 無音で視覚的フィードバック
}
}
//========================= 円ジェスチャ判定 =========================//
bool IsCircleGesture(const std::vector<POINT>& pts) {
if ((int)pts.size() < MIN_POINTS) return false;
// 総距離
double len = 0.0;
for (size_t i = 1; i < pts.size(); ++i)
len += Dist(pts[i - 1].x, pts[i - 1].y, pts[i].x, pts[i].y);
if (len < MIN_PATH_LEN) return false;
// 重心
double cx = 0.0, cy = 0.0;
for (const auto& p : pts) { cx += p.x; cy += p.y; }
cx /= pts.size(); cy /= pts.size();
// 半径分布(円らしさ)
std::vector<double> rs; rs.reserve(pts.size());
for (const auto& p : pts) rs.push_back(Dist(cx, cy, (double)p.x, (double)p.y));
double mean = 0.0; for (double r : rs) mean += r; mean /= rs.size();
if (mean < 10.0) return false; // 小さすぎる円はノイズ扱い
double var = 0.0; for (double r : rs) { const double d = r - mean; var += d * d; } var /= rs.size();
const double relStd = std::sqrt(var) / mean;
if (relStd > MAX_REL_RADIUS_STD) return false;
// 累積回転角
const double PI = std::numbers::pi_v<double>;
double sumAng = 0.0;
for (size_t i = 1; i < pts.size(); ++i) {
const double a1 = Angle(cx, cy, pts[i - 1].x, pts[i - 1].y);
const double a2 = Angle(cx, cy, pts[i].x, pts[i].y);
double d = a2 - a1;
// [-pi, pi]へ正規化
while (d > PI) d -= 2 * PI;
while (d < -PI) d += 2 * PI;
sumAng += d;
}
if (std::fabs(sumAng) < MIN_SWEEP_RAD) return false;
// 長さも十分(概ね円周の6割以上)ならOK(粗チェック)
const double approxCirc = 2 * PI * mean * 0.6;
if (len < approxCirc) return false;
return true;
}
//========================= 低レベルマウスフック =========================//
LRESULT CALLBACK MouseLLProc(int code, WPARAM wp, LPARAM lp) {
if (code == HC_ACTION) {
const MSLLHOOKSTRUCT* e = reinterpret_cast<MSLLHOOKSTRUCT*>(lp);
switch (wp) {
case WM_RBUTTONDOWN: {
// 5連打カウント
const DWORD now = GetTickCount();
if (g_firstTapTick == 0 || (now - g_firstTapTick) > TAP_WINDOW_MS) {
g_firstTapTick = now; g_tapCount = 1;
} else {
if (++g_tapCount >= TAP_COUNT_TARGET) {
g_firstTapTick = 0; g_tapCount = 0;
FireCopy();
}
}
// 円ジェスチャ準備
g_rbtnDown = true;
g_path.clear();
g_path.push_back(e->pt);
break;
}
case WM_MOUSEMOVE:
if (g_rbtnDown) g_path.push_back(e->pt);
break;
case WM_RBUTTONUP:
if (g_rbtnDown) {
g_rbtnDown = false;
if (IsCircleGesture(g_path)) FireCopy();
g_path.clear();
}
break;
default: break;
}
}
return CallNextHookEx(g_hMouseLL, code, wp, lp);
}
//========================= タスクトレイ:アイコン&メニュー =========================//
// アイコンの追加(起動時)
void TrayAdd(HWND hwnd) {
g_nid = {};
g_nid.cbSize = sizeof(g_nid);
g_nid.hWnd = hwnd;
g_nid.uID = 1;
g_nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
g_nid.uCallbackMessage = WMAPP_TRAYICON;
g_nid.hIcon = LoadIcon(nullptr, IDI_INFORMATION);
lstrcpynW(g_nid.szTip, L"日付コピー(左クリック=コピー / 右クリック=メニュー)", _countof(g_nid.szTip));
Shell_NotifyIconW(NIM_ADD, &g_nid);
}
// アイコン削除(終了時)
void TrayDelete() {
if (g_nid.cbSize) {
Shell_NotifyIconW(NIM_DELETE, &g_nid);
g_nid = {};
}
}
// 右クリックメニューをその場に表示
void TrayShowContextMenu(HWND hwnd) {
POINT pt; GetCursorPos(&pt);
HMENU hMenu = CreatePopupMenu();
if (hMenu) {
// メニュー構築
AppendMenuW(hMenu, MF_STRING, ID_TRAY_COPY, L"YYMMDD");
AppendMenuW(hMenu, MF_STRING, ID_TRAY_ABOUT, L"バージョン情報 mk 2025 ChatGPT");
AppendMenuW(hMenu, MF_SEPARATOR, 0, nullptr);
AppendMenuW(hMenu, MF_STRING, ID_TRAY_EXIT, L"終了");
// トレイメニューはフォアグラウンドで出さないと閉じ挙動が乱れる
SetForegroundWindow(hwnd);
TrackPopupMenu(hMenu, TPM_RIGHTBUTTON, pt.x, pt.y, 0, hwnd, nullptr);
DestroyMenu(hMenu);
}
}
//========================= メイン隠しウィンドウ(メッセージ拠点) =========================//
//サブシステムを WINDOWS に変更(最も確実)
//MSVC のプロジェクト設定 → Linker → System → Subsystem = Windows
LRESULT CALLBACK MainWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_CREATE: {
// 低レベルマウスフック開始
g_hMouseLL = SetWindowsHookExW(WH_MOUSE_LL, MouseLLProc, GetModuleHandleW(nullptr), 0);
// トレイアイコン追加
TrayAdd(hWnd);
return 0;
}
case WMAPP_TRAYICON: {
// 通知領域からのイベント
switch (LOWORD(lParam)) {
case WM_LBUTTONUP:
case WM_LBUTTONDBLCLK:
// 左クリック=即コピー
FireCopy();
break;
case WM_RBUTTONUP:
case WM_CONTEXTMENU:
// 右クリック=メニュー展開
TrayShowContextMenu(hWnd);
break;
default: break;
}
return 0;
}
case WM_COMMAND: {
// トレイメニュー項目
switch (LOWORD(wParam)) {
case ID_TRAY_COPY:
FireCopy();
break;
case ID_TRAY_ABOUT:
// 無音で情報を出したいが、簡易にはMessageBox(音は鳴らない)
MessageBoxW(hWnd, L"mk 2025 ChatGPT", L"バージョン情報", MB_OK | MB_TOPMOST);
break;
case ID_TRAY_EXIT:
PostQuitMessage(0);
break;
default: break;
}
return 0;
}
case WM_QUERYENDSESSION:
// OSシャットダウン/ログオフ開始 → OKを返して後始末(WM_ENDSESSIONへ続く)
return TRUE;
case WM_ENDSESSION:
if (wParam) {
// セッション終了確定:ここで後始末
if (g_hMouseLL) { UnhookWindowsHookEx(g_hMouseLL); g_hMouseLL = nullptr; }
TrayDelete();
}
return 0;
case WM_DESTROY:
// 通常終了時の後始末
if (g_hMouseLL) { UnhookWindowsHookEx(g_hMouseLL); g_hMouseLL = nullptr; }
TrayDelete();
PostQuitMessage(0);
return 0;
default:
return DefWindowProcW(hWnd, msg, wParam, lParam);
}
}
//========================= エントリポイント(/SUBSYSTEM:WINDOWS) =========================//
int APIENTRY wWinMain(HINSTANCE hInst, HINSTANCE, LPWSTR, int) {
// メイン(非表示)ウィンドウを 1つ作り、そこにフック/トレイ/OSD等を集約
WNDCLASSW wc{};
wc.lpfnWndProc = MainWndProc;
wc.hInstance = hInst;
wc.lpszClassName = kMainWndClass;
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
RegisterClassW(&wc);
HWND hwnd = CreateWindowExW(
0, kMainWndClass, L"", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 240, 120,
nullptr, nullptr, hInst, nullptr);
// 完全非表示で常駐
ShowWindow(hwnd, SW_HIDE);
// メッセージループ(終了=PostQuitMessage)
MSG msg;
while (GetMessageW(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
return (int)msg.wParam;
}