博多南ウェブサービスのblog

博多南ウェブサービスのサービス紹介

UWSC を PowerShell Core 7.3 に置き換えたときのメモ

やりたいこと

  1. Windows11 で表示した Window (Window1)のうち、(最小化ではなく)背面に隠れている画面のキャプチャを取得(画像1)
  2. UWSC の ChkImgX で、取得した画像1とターゲット画像(画像2)を比較し、画像2が画像1の中にあればその位置(位置1)を取得
  3. UWSC で、取得した位置1をもとに、Windows1 の該当する箇所をクリック

といった、自動化をやりたい。

ただ、Window1 が背面にある場合、ChkImgX で画面のキャプチャが取得できない(真っ黒画面が取得される)ことが分かった(これは、Window1側の設定によるものなのかは不明。)

そこで、Powershell Core 7.3 + C++ (14?)でやってみたときのメモです。

やったこと

  1. 以下を実施するdll(独自dll ) を、C++ で作成
    1. Win32API の PrintWindow を呼び出して、(最小化ではなく)背面に隠れている画面のキャプチャを取得(画像1)
    2. OpenCV 4.7 で、取得した画像1とターゲット画像(画像2)を比較し、画像2が画像1の中にあればその位置(位置1)を取得
  2. Powershell から、独自dll を呼び出して、位置1を取得
  3. (今回対象の Window が Windows Subsystem for Android のアプリだったので)Powershell から、位置1をタップするコマンド(adb.exe ***)を実行

初めてのC++ステップ

C++ について、初めて触れる → 独自dll 作成までの各ステップ

  1. Vissual Studio 2022 をインストールする
  2. マイクロソフトのLearning を参考に、独自dll とそれを呼び出すコンソールアプリ(アプリ1)を作成する
  3. このタイミングで、独自dll のRelease バージョンビルド後、Powershell から呼び出せることを確認する(でないと、どこが悪いかわからなくなるので)
  4. デバッグ用として、独自dll からの結果をアプリ1で表示する方法、またはVisual Studio 2022 のデバッガを起動して変数の値を表示する方法を確認する
  5. PrintWindow 関数を呼び出すときに必要なお作法(GetDC でデバイスコンテキスト取得したら、処理完了後、プログラム上で責任もって解放する(ReleaseDC)等)を調べる
  6. PrintWindow 関数を呼び出して、画像1が取得できることを確認する(クリップボードに保存が簡単だった)
  7. 画像1を、OpenCV で使用するMat 型に変換し、変換できていることを確認する(画像ファイルに保存が簡単だった)
  8. OpenCV の matchTemplate 関数を使用して位置1を取得し、取得できていることを確認する(OpenCV のサンプルコードが参考になった)
  9. 独自dll のRelease バージョンビルド後、Powershell から呼び出せることを確認する

実際のコード

ChkImgCpp.h

#pragma once

#define CHKIMGCPP_API __declspec(dllexport)

#include <windows.h>
#include <string>

struct CHKIMGCPP_API ReturnPoint {
    int doneMatching; // 0: not done template matching for some errors, 1: done template matching
    int maxValInt; // maxVal of temaplate matching scaled 0 to 100
    int x;
    int y;
};

extern "C" CHKIMGCPP_API ReturnPoint __stdcall point_template_matched_bitmap(
    const wchar_t* class_name,
    const wchar_t* window_title,
    const wchar_t* bitmap_file_path
);

ChkImgCpp.cpp

#include "pch.h" // use stdafx.h in Visual Studio 2017 and earlier

#include <windows.h>
#include <dwmapi.h>

#include <string>

#include "ChkImgCpp.h"
#include <opencv2/opencv.hpp>

/**
 *  WindowDCを解放するためのdeleter
 * https://setuna-kanata.hatenadiary.org/entry/20100713/1279039787
 */
struct dc_deleter_release
{
    void operator()(HDC hdc)const
    {
        ReleaseDC(hwnd, hdc);
    }

    dc_deleter_release(HWND hwnd) : hwnd(hwnd) {}

    HWND hwnd;
};
struct dc_deleter_delete
{
    void operator()(HDC hdc)const
    {
        DeleteDC(hdc);
    }

    dc_deleter_delete() {}
};
struct hbitmap_deleter_delete
{
    void operator()(HBITMAP hBitmap)const
    {
        DeleteObject(hBitmap);
    }

    hbitmap_deleter_delete() {}
};

// https://gist.github.com/siwa32/98704
std::string convString(const std::wstring& input)
{
    size_t i;
    char* buffer = new char[input.size() * MB_CUR_MAX + 1];
    wcstombs_s(&i, buffer, input.size() * MB_CUR_MAX + 1, input.c_str(), _TRUNCATE);
    std::string result = buffer;
    delete[] buffer;
    return result;
}

ReturnPoint __stdcall point_template_matched_bitmap(
    const wchar_t* class_name,
    const wchar_t* window_title,
    const wchar_t* bitmap_file_path
)
{
    ReturnPoint r{};
    r.doneMatching = 0; // 0: not done template matching for some errors, 1: done template matching

    HWND hwnd = FindWindow(class_name, window_title);
    if (hwnd == NULL)
    {
        return r;
    }

    RECT rect{};
    if (DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &rect, sizeof(rect)) != S_OK)
    {
        return r;
    }

    // ReleaseDC 等を呼ばなくてすむように、std::unique_ptr を利用
    // https://setuna-kanata.hatenadiary.org/entry/20100713/1279039787
    std::unique_ptr<HDC__, dc_deleter_release> hdc(::GetDC(hwnd), dc_deleter_release(hwnd));

    // Bitmap to cv::Mat
    // http://ckron.net/?p=205
    BITMAPINFO bmpInfo{};
    bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    bmpInfo.bmiHeader.biWidth = rect.right - rect.left;
    bmpInfo.bmiHeader.biHeight = rect.bottom - rect.top;
    bmpInfo.bmiHeader.biPlanes = 1;
    bmpInfo.bmiHeader.biBitCount = 32;
    bmpInfo.bmiHeader.biCompression = BI_RGB;

    LPDWORD lpPixel{};
    std::unique_ptr<HBITMAP__, hbitmap_deleter_delete>
        hBitMap(CreateDIBSection(hdc.get(), &bmpInfo, DIB_RGB_COLORS, (void**)&lpPixel, NULL, 0), hbitmap_deleter_delete());
    if (hBitMap.get() != NULL)
    {
        // メモリDC
        std::unique_ptr<HDC__, dc_deleter_delete> hdcMem(CreateCompatibleDC(hdc.get()), dc_deleter_delete());

        SelectObject(hdcMem.get(), hBitMap.get());
        PrintWindow(hwnd, hdcMem.get(), PW_RENDERFULLCONTENT);
    }

    BITMAP bmp{};
    GetObject(hBitMap.get(), sizeof(BITMAP), &bmp);

    /*
   // for debugging bitmap data
   // http://www13.plala.or.jp/kymats/study/MULTIMEDIA/convert_dib_ddb.html
   if (OpenClipboard(hwnd))
   {
       HBITMAP hb;
       hb = CreateDIBitmap(hdc.get(), &bmpInfo.bmiHeader, CBM_INIT, lpPixel, &bmpInfo, DIB_RGB_COLORS);
       EmptyClipboard();
       SetClipboardData(CF_BITMAP, hb);
       CloseClipboard();
   }
   return r;
   */

    // PrintWindow で取得した画像は32bitカラーイメージだったので決め打ち
    cv::Mat revMat(bmp.bmHeight, bmp.bmWidth, CV_8UC4, (void*)bmp.bmBits);
    cv::Mat mat{};
    cv::flip(revMat, mat, 0);
    if (!mat.data)
    {
        return r;
    }

    // 比較ファイル
    cv::Mat target = cv::imread(convString(bitmap_file_path), 1);
    if (!target.data)
    {
        return r;
    }

    // template matching
    cv::Mat matBGR{};
    cv::cvtColor(mat, matBGR, cv::COLOR_BGRA2BGR);

    int result_cols = matBGR.cols - target.cols + 1;
    int result_rows = matBGR.rows - target.rows + 1;

    cv::Mat result{};
    result.create(result_rows, result_cols, CV_8UC3);

    cv::matchTemplate(matBGR, target, result, cv::TM_CCOEFF_NORMED);

    double minVal{};
    double maxVal{};
    cv::Point minLoc;
    cv::Point maxLoc;
    cv::Point matchLoc;

    minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc, cv::Mat());
    matchLoc = maxLoc;

    r.doneMatching = 1; // 0: not done template matching for some errors, 1: done template matching
    r.maxValInt = maxVal * 100;
    r.x = matchLoc.x + target.cols / 2;
    r.y = matchLoc.y + target.rows / 2;

    /*
   //for debugging matched point
   cv::Mat img_display{};
   matBGR.copyTo(img_display);

   rectangle(img_display, matchLoc, cv::Point(matchLoc.x + target.cols, matchLoc.y + target.rows), cv::Scalar::all(0), 2, 8, 0);

   imshow("debugging matched point", img_display);
   cv::waitKey(0);
   //*/

    return r;
}

Powershell で呼び出す

# [ChkImgCpp]::point_template_matched_bitmap returns ReturnPoint for window (also class name) and bitmap_file_path.
# Return Point has
#  - int doneMatching; // 0: not done template matching for some errors, 1: done template matching
#  - int maxValInt; // maxVal of temaplate matching scaled 0 to 10000
#  - int x; // center x position of maxVal location
#  - int y; // center y position of maxVal location
$dllFilePathEscaped = (Join-Path -Path ${PSScriptRoot} -ChildPath "ChkImgCpp.dll").Replace('\', '\\')
$source = @"
using System.Runtime.InteropServices;
public struct ReturnPoint {
    public int doneMatching; // 0: not done template matching for some errors, 1: done template matching
    public int maxValInt; // maxVal of temaplate matching scaled 0 to 100
    public int x;
    public int y;
}
public static class ChkImgCpp
{
    [DllImport("${dllFilePathEscaped}", CharSet=CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
    public static extern ReturnPoint point_template_matched_bitmap(
        System.String class_name,
        System.String window_title,
        System.String bitmap_file_path
    );
}
"@
Add-Type -TypeDefinition $source;

$local:class_name_of_window = "<ウィンドウの class name: UWSC で取得が簡単>"
$local:title_of_window = "<ウィンドウの タイトル>"
$local:bmp_relative_path_non_escaped = "<比較するbmp file (24bit) の相対パス>"

$local:bmpFilePathEscaped = (Join-Path -Path ${PSScriptRoot} -ChildPath $bmp_relative_path_non_escaped).Replace('\', '\\')
$returnPoint = [ChkImgCpp]::point_template_matched_bitmap($class_name_of_window, $title_of_window, $bmpFilePathEscaped)

Write-Output $returnPoint  

つまづいた点

  • dll、lib、exe の違いって何?
  • ヘッダファイル、ソースファイルって何?
  • dll ってどこにある?どこにおく?
  • ビルドすると何が出力される?dll だけ?lib、exe も?
  • C++ のstring って何?どうやって関数に渡すの?ポインタの概念はなんとなくわかるけど・・
  • GetDC のあとは必ずReleaseDC しろって、何かいい方法はないの?
  • Win32API のBITMAP 型って何?save(bmp, filePath) で保存じゃないの?

参考