Perl日記

日々の知ったことのメモなどです。Perlは最近やってないです。

Spy++(spyxx.exe)をインストールしたよメモ

Windows7です。

前回のエントリにおいて、VOICEROID2プロセスにキーコードを送るために、ウィンドウハンドルやその中の構造を知る必要があった。
こういうときは、MicrosoftのSpy++を使えば良いようだ。




https://msdn.microsoft.com/ja-jp/library/dd460756.aspxmsdn.microsoft.com




と思ったのだがどこにもSpy++をダウンロードするリンクがない。
Visual Studio 2017のツールとして存在しているようだ。

(これだけのためにごついアプリを入れないといけないのもなんかアレだが)
Visual Studio Community 2017をダウンロードしてインストールした。






インストールしただけだとSpy++は入っていないので、別途インストールする。

「ツール(T)」→「ツールと機能を取得(T)...」を選んで、 出て来るUACを「はい」する。

これは「すべてのプログラム」の中にも入っている「Visual Studio Installer」を起動しているだけっぽい。

起動したら、「個別のコンポーネント」タブを選択して、以下を選択して、「変更」する。



完了したらVisual Studio 2017は全部閉じて、エクスプローラから以下に降りる。

C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\Tools

spyxx.exeを見つけてショートカットを作ってデスクトップに持ってきていつでも使えるようにする。

これでオッケー


参考

ありがとうございます。

Go言語でVOICEROID2を操作してみたかった(Windows7)

Go言語を初めてWindowsで使ったよ、初めてWindowsAPIを使ったよメモ。(Windows7です)

VOICEROID2(結月ゆかり)を買ったので、なんとかして色々連携させてみたかった。

VOICEROID2 結月ゆかり

VOICEROID2 結月ゆかり

画面スクショ

f:id:rightgo09:20180213091440p:plain

うまくいった方法

喋らせたい文字をクリップボードに入れておいて、Ctrl+Vで貼り付けて、F5キーショートカットで再生して喋らせるやり方。
なんとかできたけど、いろいろ制約ができてしまった。

逆にうまくいなかった方法

SendMessage()やPostMessage()で文字を送ったり制御コードを送るやり方。
たぶんこちらの方が正攻法だと思うのだけど…。
なんでうまくいかなかったかはちょっとよくわからなかったです…。

VOICEROID2にCtrl+Vするコード

package main

import (
	"fmt"
	"syscall"

	"github.com/AllenDang/w32"
)

const voiceroid2WindowTitle = "VOICEROID2"

var mainHwnd w32.HWND

func init() {
	// ウィンドウタイトルからVOICEROID2のウィンドウハンドルを取得する
	mainHwnd = findWindow(voiceroid2WindowTitle)
	if mainHwnd == 0 {
		mainHwnd = findWindow(voiceroid2WindowTitle + "*")
		if mainHwnd == 0 {
			panic("cannot find window of VOICEROID2")
		}
	}
}

func findWindow(wName string) w32.HWND {
	return w32.FindWindowW(nil, syscall.StringToUTF16Ptr(wName))
}

func SayYukari() {
	w32.SetForegroundWindow(mainHwnd)

	ctlDown, ctlUp := inputKeys(w32.VK_CONTROL, 0x001D)
	bsDown, bsUp := inputKeys(w32.VK_BACK, 0x000E)
	aDown, aUp := inputKeys(0x0041, 0x001E)
	vDown, vUp := inputKeys(0x0056, 0x002F)
	f5Down, f5Up := inputKeys(w32.VK_F5, 0x003F)
	ipts := []w32.INPUT{
		// Ctrl + A
		ctlDown, aDown, aUp, ctlUp,
		// BackSpace
		bsDown, bsUp,
		// Ctrl + V
		ctlDown, vDown, vUp, ctlUp,
		// F5
		f5Down, f5Up,
	}
	ret := w32.SendInput(ipts)
	fmt.Println("w32.SendInput result:", ret)
}

func inputKeys(vk, scan uint16) (w32.INPUT, w32.INPUT) {
	down := w32.INPUT{
		Type: w32.INPUT_KEYBOARD,
		Ki: w32.KEYBDINPUT{
			WVk:   vk,
			WScan: scan,
		},
	}
	up := w32.INPUT{
		Type: w32.INPUT_KEYBOARD,
		Ki: w32.KEYBDINPUT{
			WVk:     vk,
			WScan:   scan,
			DwFlags: 0x0002,
		},
	}
	return down, up
}

だめだったパターン…

// Aを送る
w32.SendMessage(mainHwnd, w32.WM_KEYDOWN, 0x0041, 1)
w32.SendMessage(mainHwnd, w32.WM_KEYUP, 0x0041, 0x6000001)

やってみたメモ

WindowsAPIを使ったといっても、 AllenDang/w32 を使わせてもらっただけである。
幸いこの中にあるものだけでうまくできた。

KEYBDINPUT構造体を押したいキーの分だけ作って、SendInput()で送る。

バーチャルキーコード(wvk)は、msdnのページを見たり、よくあるやつ(Aは41とか)ですぐわかったけど、スキャンコード(wscan)はよくわからなかったので、spy++で実際に送られてる中身を覗き見てそれをそのまま使った。



SendInput()はSendMessage()などと違って、ウィンドウハンドルを指定できない。
つまりその時点でアクティブになっているウィンドウに対してキーが送られる。
なので、邪道な感じだけど、喋らせるときにVOICEROID2ウィンドウをSetForegroundWindow()でアクティブにしている。
更に邪道な感じで、Ctrl+Vがうまく動くためには、テキスト入力枠にフォーカスが当たってCtrl+Vが押せる状態でなければならない。
(これどうしたらいいんだろうか?)


VOICEROID2は中身が一個のウィンドウっぽくて、メモ帳で試したときとは違い、入れ子のウィンドウになっていなかった。メモ帳は"Edit"というクラス名の子ウィンドウにキーをSendMessage()すれば動いた。VOICEROID2には唯一の親ウィンドウに送ればいいということかと思ったが、そういう感じでもないっぽい? よくわからなかった。


ZoomのAPIを使ってミーティングを作成する

Zoomメモ。

会社ではリモートワークしてるみんな(自分含む)とミーティングするのに、Zoomが使われている。

zoom.us


ZoomにはAPIがあるので、ミーティング用のURL作成処理をGo言語で作ってみた。

今回は60日トライアルでデベロッパー登録をした。

https://developer.zoom.us/
https://zoom.github.io/api/#create-a-meeting

API KEYとAPI Secret(とAccess Token)を作って控えておく。

ちなみにバージョンはv2を使う。

JWTを使う

import jwt "github.com/dgrijalva/jwt-go"

const ZOOM_API_KEY = "xxx"
const ZOOM_API_SECRET = "xxx"

token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
	"iss": ZOOM_API_KEY,
	"exp": fmt.Sprintf("%d", time.Now().Add(1*time.Minute).Unix()),
})

tokenString, err := token.SignedString([]byte(ZOOM_API_SECRET))

fmt.Println(tokenString)

1分間だけ有効なJWTができた。

自分のuser_idを知る

JWTをcurlコマンドでusersのAPIを叩いてみる。

$ curl -H 'Authorization: Bearer xxxxxx' https://api.zoom.us/v2/users | jq .
{
  "page_count": 1,
  "page_number": 1,
  "page_size": 30,
  "total_records": 1,
  "users": [
    {
      "id": "xxx",
      "first_name": "太郎",
      "last_name": "山田",
      "email": "taro.yamada@example.com",
      "type": 1,
      "pmi": 000,
      "timezone": "Asia/Tokyo",
      "dept": "",
      "created_at": "xxx",
      "last_login_time": "xxx",
      "last_client_version": "xxx"
    }
  ]
}

自分のuser_idが分かった。

ミーティングを作る

package main

import (
	"encoding/json"
	"log"
	"net/http"
	"strings"
)

const USER_ID = "xxx"
const CREATE_MEETING_URL = "https://api.zoom.us/v2/users/" + USER_ID + "/meetings"
const JWT = "xxx"

type Meeting struct {
	UUID      string    `json:"uuid"`
	ID        int       `json:"id"`
	HostID    string    `json:"host_id"`
	Topic     string    `json:"topic"`
	Type      int       `json:"type"`
	Duration  int       `json:"duration"`
	Timezone  string    `json:"timezone"`
	CreatedAt time.Time `json:"created_at"`
	StartURL  string    `json:"start_url"`
	JoinURL   string    `json:"join_url"`
	Settings  struct {
		HostVideo           bool   `json:"host_video"`
		ParticipantVideo    bool   `json:"participant_video"`
		CnMeeting           bool   `json:"cn_meeting"`
		InMeeting           bool   `json:"in_meeting"`
		JoinBeforeHost      bool   `json:"join_before_host"`
		MuteUponEntry       bool   `json:"mute_upon_entry"`
		Watermark           bool   `json:"watermark"`
		UsePmi              bool   `json:"use_pmi"`
		ApprovalType        int    `json:"approval_type"`
		Audio               string `json:"audio"`
		AutoRecording       string `json:"auto_recording"`
		EnforceLogin        bool   `json:"enforce_login"`
		EnforceLoginDomains string `json:"enforce_login_domains"`
		AlternativeHosts    string `json:"alternative_hosts"`
	} `json:"settings"`
}

func main() {
	payload := strings.NewReader(`{"type":1}`) // 1は通常の簡易ミーティング

	req, err := http.NewRequest("POST", CREATE_MEETING_URL, payload)
	if err != nil {
		panic(err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+JWT) // ヘッダに設定

	client := http.DefaultClient
	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}

	if res.StatusCode != 201 {
		log.Fatal("status code is not 201!", res.Status)
	}

	defer res.Body.Close()
	var meeting Meeting
	err = json.NewDecoder(res.Body).Decode(&meeting)
	if err != nil {
		log.Fatal("json parse error!", err)
	}
	log.Println(meeting.JoinURL)
}

start_urlとjoin_urlの違いがいまひとつよくわからなかった(ドキュメントにも記載なし?)が、join_urlにアクセスしてmacのzoom.usアプリが立ち上がると、普通にミーティングができて開始時刻もそこからだったので、これで問題ないのかな、という感じ。