ファイル名に日時を入れています。

今日なら、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;
}