tmytのらくがき

個人の日記レベルです

今話題のMastodonインスタンスを建ててS3の代わりにAzure Blob Storageを使う

Pixivとかドワンゴインスタンスを建てて話題沸騰のMastodonですけど、僕もAzure IaaS上にインスタンスを建ててみてました。 Docker Composeでさくっと作っただけだったのを、画像アップロード先をAzure Blobにするとかしてちょっとカスタムしたりもしたのでそのお話です。

Azure Blobにする

Mastodonは標準でAmazon S3に対応してるので、S3使う人はそのまま使えばなんとなくうまくいきます。でもAzure IaaSに建てたしせっかくなのでBlob使います。

Azure BlobをS3のAPIでアクセスするには s3proxy っての使えばどうにかなります。Java製。

github.com

docker-compose.ymlにs3proxy追加しました。

diff --git a/docker-compose.yml b/docker-compose.yml
index 81c6fe9..d953d44 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,6 +15,11 @@ services:
 #    volumes:
 #      - ./redis:/data

+  s3proxy:
+    restart: always
+    image: andrewgaul/s3proxy
+    env_file: .env.production
+
   web:
     restart: always
     build: .
@@ -26,6 +31,7 @@ services:
     depends_on:
       - db
       - redis
+      - s3proxy
     volumes:
       - ./public/assets:/mastodon/public/assets
       - ./public/system:/mastodon/public/system

そして、.env.productionにS3といいつつAzure Blobにアクセスするように設定します。します。

 # S3 (optional)
-# S3_ENABLED=true
-# S3_BUCKET=
-# AWS_ACCESS_KEY_ID=
-# AWS_SECRET_ACCESS_KEY=
-# S3_REGION=
-# S3_PROTOCOL=http
-# S3_HOSTNAME=192.168.1.123:9000
+S3_ENABLED=true
+S3_BUCKET=uploads
+AWS_ACCESS_KEY_ID=local-identity
+AWS_SECRET_ACCESS_KEY=local-credential
+S3_PROTOCOL=https
+S3_HOSTNAME=***.blob.core.windows.net
+S3_ENDPOINT=http://s3proxy/
+S3_PERMISSION=private

それに加えて、s3proxyの設定も書きます。。

+S3PROXY_AUTHORIZATION=none
+S3PROXY_CORS_ALLOW_ALL=true
+JCLOUDS_PROVIDER=azureblob
+JCLOUDS_ENDPOINT=https://***.blob.core.windows.net/
+JCLOUDS_IDENTITY=***
+JCLOUDS_CREDENTIAL=****************************************

だいたいこれでいいはずなんですけど、このまま動かすとs3proxyから501 Not Implementedエラーが帰ってくるのです。なんか、調べたところによると、X-Amz-Aclヘッダにpublic-readが指定されてるとなんかよくわかんないけどAzure Blob宛てだと501 Not Implementedになるみたいです。

で、このパラメータなんですけど、config/initializers/paperclip.rbpublic-readに決め打ちされてて非常に困るので適当にパッチします。

diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb
index 77bc13b..6fab071 100644
--- a/config/initializers/paperclip.rb
+++ b/config/initializers/paperclip.rb
@@ -16,7 +16,7 @@ if ENV['S3_ENABLED'] == 'true'
   Paperclip::Attachment.default_options[:s3_host_name]   = ENV.fetch('S3_HOSTNAME') { "s3-#{ENV.fetch('S3_REGION')}.amazonaws.com" }
   Paperclip::Attachment.default_options[:path]           = '/:class/:attachment/:id_partition/:style/:filename'
   Paperclip::Attachment.default_options[:s3_headers]     = { 'Cache-Control' => 'max-age=315576000' }
-  Paperclip::Attachment.default_options[:s3_permissions] = 'public-read'
+  Paperclip::Attachment.default_options[:s3_permissions] = ENV.fetch('S3_PERMISSION') { 'public-read' }
   Paperclip::Attachment.default_options[:s3_region]      = ENV.fetch('S3_REGION') { 'us-east-1' }

これでdocker-compose buildしてdocker-compose up -d するといい感じになりました。

14393のトーストの挙動メモ

Windows 10 14393のトーストの挙動、特に画像周りのメモ。

  • <image placement=“inline” /> が1個
<toast>
  <visual>
    <binding template="ToastGeneric">
      <text>Tortoise beats rabbit in epic race</text>
      <text>In a surprising turn of events, Rockstar Rabbit took a nasty crash, allowing Thomas the Tortoise to win the race.</text>
      <image placement="inline" src="https://unsplash.it/360/180?image=1043"/> 
      <text placement="attribution">The Animal Times</text>
    </binding>
  </visual>
</toast>

f:id:tmyt:20170305130149p:plain

f:id:tmyt:20170305130255p:plain

f:id:tmyt:20170305130331p:plain

  • <image placement=“inline” /> が複数個
<toast>
  <visual>
    <binding template="ToastGeneric">
      <text>Tortoise beats rabbit in epic race</text>
      <text>In a surprising turn of events, Rockstar Rabbit took a nasty crash, allowing Thomas the Tortoise to win the race.</text>
      <image placement="inline" src="https://unsplash.it/360/180?image=1043"/> 
      <image placement="inline" src="https://unsplash.it/360/180?image=1043"/> 
      <image placement="inline" src="https://unsplash.it/360/180?image=1043"/> 
      <text placement="attribution">The Animal Times</text>
    </binding>
  </visual>
</toast>

f:id:tmyt:20170305130427p:plain

f:id:tmyt:20170305130515p:plain

f:id:tmyt:20170305130526p:plain

  • <image placement=“hero” /> が1個
<toast>
  <visual>
    <binding template="ToastGeneric">
      <text>Tortoise beats rabbit in epic race</text>
      <text>In a surprising turn of events, Rockstar Rabbit took a nasty crash, allowing Thomas the Tortoise to win the race.</text>
      <image placement="hero" src="https://unsplash.it/360/180?image=1043"/> 
      <text placement="attribution">The Animal Times</text>
    </binding>
  </visual>
</toast>

f:id:tmyt:20170305130634p:plain

f:id:tmyt:20170305130721p:plain

f:id:tmyt:20170305130735p:plain

  • <image placement=“hero” /> が複数個
<toast>
  <visual>
    <binding template="ToastGeneric">
      <text>Tortoise beats rabbit in epic race</text>
      <text>In a surprising turn of events, Rockstar Rabbit took a nasty crash, allowing Thomas the Tortoise to win the race.</text>
      <image placement="hero" src="https://unsplash.it/360/180?image=1043"/> 
      <image placement="hero" src="https://unsplash.it/360/180?image=1043"/> 
      <image placement="hero" src="https://unsplash.it/360/180?image=1043"/> 
      <text placement="attribution">The Animal Times</text>
    </binding>
  </visual>
</toast>

f:id:tmyt:20170305130826p:plain

f:id:tmyt:20170305130944p:plain

f:id:tmyt:20170305130955p:plain

  • <image placement=“inline” /> と <image placement=“hero” /> の組み合わせ
<toast>
  <visual>
    <binding template="ToastGeneric">
      <text>Tortoise beats rabbit in epic race</text>
      <text>In a surprising turn of events, Rockstar Rabbit took a nasty crash, allowing Thomas the Tortoise to win the race.</text>
      <image placement="hero" src="https://unsplash.it/360/180?image=1043"/> 
      <image placement="inline" src="https://unsplash.it/360/180?image=1043"/> 
      <image placement="inline" src="https://unsplash.it/360/180?image=1043"/> 
      <text placement="attribution">The Animal Times</text>
    </binding>
  </visual>
</toast>

f:id:tmyt:20170305131156p:plain

f:id:tmyt:20170305131258p:plain

f:id:tmyt:20170305131312p:plain

FIDO 2.0を実装してみた感じ

TL;DR

  • Passport.jsでFIDO 2.0実装してみた
  • EdgeでWindows Helloした
  • あとあとよく見たらpassport-mspassport*1ってのがありました

やりたかったこと

WebサービスWindows Hello認証をやってみたくていろいろ試してみた感じ。Windows HelloはFIDO 2.0の実装となっていて、公開鍵暗号をベースとしたチャレンジレスポンス認証。

あくまでもユーザとデバイスの組の確実性しか保証されないので、ログインしようとしているユーザがリソースにアクセスする権限があるかどうかについては関与できない。なので、実際に使うとなると既知のユーザアカウント認証に対して公開鍵を登録するという実装になるのかな。という感じ。 今回実装したものも、GitHubOAuth認証がすでに実装されているのでそこに対してFIDO 2.0の公開鍵を登録する仕組みにしました。

GitHub認証はPassport.jsで実装しているのでWindows Hello認証もそこに混ぜてみることにしました。

用意したもの

  • express.js
  • passport.js
  • passport-fido2*2
  • webauthn.js

実装

Passport.jsのStrategyを作るには、Passport-strategyを継承してauthenticate関数を実装すればいいらしい。 そんでもって、Windows HelloとかFIDO2.0とかでぐぐるとNodeな実装がちらほら見つかるのでいろいろ参考にしながら実装したものがこちら。

github.com

サーバが送信したChallengeであることを検証するためにChallengeをHMAC-SHA256したものをクライアントから送信させているのに、 検証に使っていない…のはそのうち修正するとして…

このStrategyは送信されたIDに対応するサーバにすでに格納されている公開鍵を使ってリクエストを検証するところまでしか面倒を見ません。 なので実際に使うには既知のアカウントでログインし、公開鍵を保存する必要があります。 その時のクライアント実装はこういう感じにしてます。

const p = this.context.profile;
const account = { rpDisplayName: p.username, displayName: p.username, imageUri: p.avater };
const cryptoParams = [{ type: 'FIDO_2_0', algorithm: 'RSASSA-PKCS1-v1_5' }];
navigator.authentication.makeCredential(account, cryptoParams)
.then(result => {
  const key = JSON.stringify(result.publicKey);
  const id = result.credential.id;
  Http.post('/api/credentials/fido/register', {key, id},
    () => Toast.show('FIDO key registered', 'success'),
    () => Toast.show('Key registration failed', 'warning')
  );
  console.log(result);
});

認証済みエリアに証明書登録ボタンをつくって、最初にこれで登録。そのあとログイン画面に移ると

sign(challenge){
  const q = qs.parse(this.context.router.location.search.substring(1));
  navigator.authentication.getAssertion(challenge.c)
  .then(result => {
    const uri = '/auth/fido2?'
      + `c=${challenge.c}&cs=${challenge.cs}&`
      + `authenticatorData=${result.authenticatorData}&`
      + `clientData=${result.clientData}&`
      + `signature=${result.signature}&`
      + `id=${result.credential.id}`
      + (q._redir ? `&_redir=${encodeURIComponent(q._redir)}` : '');
    window.location.href = uri;
  });
}
authenticate(){
  Http.get('/auth/fido2/challenge', {},
    c => this.sign(c),
    () => Toast.show('Failed to exchange challenge', 'error')
  );
}

ってやってます。最初にサーバからチャレンジをもらって、それに対して秘密鍵で署名。そのあと、passport-fido2を呼び出しています。

Edgeのfido2周りの実装が結構標準から離れてるらしく、Webauthn.jsを使ったほうがいいよってid:shibayan に教えてもらったりしたので使ったほうがよさそうです。 最新のドラフトと見比べてみてもRequiredな引数がRequiredじゃなかったりするし、もう少し変更ありそうな気もしたり。

まとめ

よくよく探したらpassport-mspassport ってのがありました。

github.com

ブラウザでTerminal実装してみたら簡単だった

新年あけましておめでとうございます。本年もどうぞよろしくお願いいたします。

というわけで、Webブラウザで動くターミナルを実装してみたんです。NodeJSで。そしたらすごく簡単だった。って話です。

使うもの

  • express
  • pty.js
  • socket.io
  • xterm.js
  • pug-static

インストール

npmでてきとうにインストールします。

npm init
npm install --save express pty.js pug-static socket.io xterm

サーバ実装

適当にかきます。xterm.jsの中身をexpress.staticで公開しつつ、viewはpugで書きます。

ブラウザとの通信はSocket.IOを使って、ブラウザとターミナルの間をそれぞれ中継してあげます。

'use strict';

const express = require('express')
    , app = express()
    , server = require('http').createServer(app)
    , pugStatic = require('pug-static')
    , Io = require('socket.io')
    , pty = require('pty.js')

app.use('/xterm.js', express.static('node_modules/xterm'))
app.use('/', pugStatic('views'))

let io = new Io(server);
io.on('connect', socket => {
  let term = pty.spawn('bash', [], {
    name: 'xterm-256color',
    cols: 80,
    rows: 24
  });
  term.on('data', d => socket.emit('data', d));
  socket.on('data', d => term.write(d));
  socket.on('disconnect', () => term.destroy());
});

server.listen(3000);

クライアント実装

こっちもてきとうに。これは、index.pugという名前でviewsディレクトリの中に保存して使います。

サーバ実装と同様に、Socket.IOでブラウザへの入出力を中継してあげます。

doctype html
html
  head
    link(rel='stylesheet', href='/xterm.js/dist/xterm.css')
  body
    #terminal
    script(src='/xterm.js/dist/xterm.js')
    script(src='/socket.io/socket.io.js')
    script.
      var term = new Terminal();
      var socket = io();
      term.open(document.getElementById('terminal'));
      term.on('data', d => socket.emit('data', d));
      socket.on('data', d => term.write(d));

おわり

あとはアクセスすると、bashが見れて、そのまま使えます。xterm.jsすごくって、vimとかtmuxとかちゃんと使えます。

ただ、この実装にユーザ認証が含まれていないので必ずユーザ認証して使ってください。

Surface Dialを2個接続してSOUND VOLTEXごっこした人へ

Surface Dialを2個以上接続していたとしても、正規のAPIからアクセスするといくつあっても1個にしか見えないのは周知の事実です。じゃぁどうにかして、2個認識できないのかなぁ…ということでやってみました。

TL;DR

  • 通常のAPIからは1個しかみえないが、HIDデバイスなので直接読めば読める
  • VID:045E, PID: 091B, UsagePage: 0001, Usage: 000Eを読めば生データ見える
  • 生データの2バイト目の下位1bitが押し下げフラグ、3バイト目が回転量(signed)、4バイト目が回転方向(00:右、FF:左)

バイス構成

f:id:tmyt:20161127132805p:plain

これを見ると、HID over GATTで5種類のデバイスが見えていて、UsagePage, Usageは次の通り

UsagePage Usage
0001 0080
0001 000E
0001 0072
FF07 0070
FF07 0071

一番上はWinRTでブロックされているUsageなので今回は省略。ほかの4つは特にWinRTでブロックされてない(!!)のでアクセスしてみたところ、返事が返ってきたのは2個目のやつでした。

とりあえずアクセス

public MainPage()
{
    this.InitializeComponent();

    Loaded += async (sender, args) =>
    {
        var d2 = await GetAsync(0x0001, 0x000E);
        d2.InputReportReceived += D2_InputReportReceived;
    };
}

private void D2_InputReportReceived(HidDevice sender, HidInputReportReceivedEventArgs args)
{
    var dump = string.Join(" ", args.Report.Data.ToArray().Select(b => $"{b:X2}"));
    Debug.WriteLine($"D2: {dump}");
}

async Task<HidDevice> GetAsync(ushort up, ushort uid)
{
    var str = HidDevice.GetDeviceSelector(up, uid);//, 0x045E, 0x091B);
    var devices = (await DeviceInformation.FindAllAsync(str)).ToArray();
    return await HidDevice.FromIdAsync(devices[0].Id, FileAccessMode.Read);
}

こんなの書いて、ダイアルを回したり押したりするとこんなダンプがデバッグ出力で得られます。

D2: 01 02 02 00 0A 0B 0C 0D 3A
D2: 01 02 03 00 0A 0B 0C 0D 3A
D2: 01 02 02 00 0A 0B 0C 0D 3A
D2: 01 02 FF FF 0A 0B 0C 0D 3A
D2: 01 02 FF FF 0A 0B 0C 0D 3A
D2: 01 02 FE FF 0A 0B 0C 0D 3A
D2: 01 02 FF FF 0A 0B 0C 0D 3A
D2: 01 02 FD FF 0A 0B 0C 0D 3A
D2: 01 02 FD FF 0A 0B 0C 0D 3A
D2: 01 02 FD FF 0A 0B 0C 0D 3A
D2: 01 02 FC FF 0A 0B 0C 0D 3A
D2: 01 02 FB FF 0A 0B 0C 0D 3A

解析結果

このバイナリの中身をよく見てるとだいたいこんな構造みたい。

struct Report{
  byte One;
  byte Flags;
  sbyte Degree;
  byte Orientation;
  byte[6] Nazo;
}

2バイト目のFlagsとして名前を付けてみたところは、ビットフィールドになっているようでそれぞれ次の意味っぽい。

ビット 意味
1ビット目 押し下げ状態。1で押してる状態
2ビット目 回転中?

3バイト目は符号付の値のようで、右回転で正の値、左回転で負の値になってる感じ。WinRTのデフォルトより元気で1度ごとにレポートがきます。

4バイト目は回転方向で、右回転なら00、左回転ならFFが入っています。

で、のこった6バイトはよくわからん。

まとめ

SOUND VOLTEXごっこできそうですね!

Surface Dialを2個接続するとどーなるの?

Q. Surface Dialって2個接続するとどーなるの?

BLEなHIDデバイスなので普通に接続できるはずだけどいったいどうなるの…?

A. APIからは1個に見える

var controller = RadialController.CreateForCurrentView();
controller.RotationChanged += (_, e) => {
  Debug.WriteLine(e.RotationDeltaInDegrees );
};

こうした時に、1つ目を回しても、2つ目を回してもそれぞれの回転量に応じた値がここでよばれる。 残念ながら別々のダイアルとして扱うことはできないみたい。

ちなみに、デバイスマネージャーからはこうみえてます。 f:id:tmyt:20161127132805p:plain

なのでいちおうAPIより下では別々のものにみえてました。

C++/Win32からSurface Dialをさわってみたよ

TL;DR

  • C++/Win32からRadialControllerなんて触るもんじゃない
  • ABI::Windows::Foundation::Collectionsに対する実装はSDKに含まれない
  • MIDLコンパイラ/ns_prefixが必須

AzureaでSurface Dialを扱えるプラグインを書きました

Surface Studioと同時に発表されたSurface Dial、回せて、クリックできるだけのデバイスなのです。 これがMicrosoft Global Summitへ参加したタイミングでちょうど発売されて運よく入手することができたので、 UWP*1も、WPF*2も、やってる人がすでにいるので面白そうなのでC++から触ってみました。

ただただ触ってみるだけじゃ面白くないので、せっかくだから2010年にWindows Mobile 5/6向けにリリースしたTwitterクライアントでSurface Dialが使えるようになるプラグインを書きました。

Surface Dialを制御するCOMオブジェクトを実装して、さらにそれをJScriptから制御してるって感じです。

gist.github.com

デスクトップアプリからSurface Dialを操作する

Surface DialをはじめとするRadialControllerははWinRT経由でOSに統合されているので、UWPからならとても簡単に扱えます。たとえばこんな感じに。

var controller = RadialController.CreateForCurrentView();
controller.RotationChanged += (sender, e) => { }
controller.ButtonClicked += (sender, e) => { }

このAPIですが、シグネチャを見てわかるように、UWPのViewを必要としています。 しかし、デスクトップアプリからだとViewが存在しないのでウィンドウハンドルを使ってインスタンスを生成できるなにがしが必要になってきます。 といっても、.NETからならそれほど大変なものでもなく、COMオブジェクトを経由してインスタンスを生成することができます。*3

[System.Runtime.InteropServices.Guid("1B0535C9-57AD-45C1-9D79-AD5C34360513")]
[System.Runtime.InteropServices.InterfaceType(System.Runtime.InteropServices.ComInterfaceType.InterfaceIsIInspectable)]
interface IRadialControllerInterop
{
    RadialController CreateForWindow(
    IntPtr hwnd,
    [System.Runtime.InteropServices.In]ref Guid riid);
}

これでインスタンスを取得すればだいたいどうにかなります。ですが、なかなかそれなりにつらいよ。ってことが以下で共有されています。

grabacr.net

ところがどっこい、C++だともう少したくさんめんどい。まず、ヘッダファイルとかはWindows SDK 14393に含まれています。 デスクトップC#だと自前定義していたIRadialControllerInteropもSDKで定義があります。なので、インスタンスの取得はこういう感じ。

// IRadialControllerIntropを取得して
Microsoft::WRL::ComPtr<IRadialControllerInterop> controllerIntrop;
Windows::Foundation::GetActivationFactory(
    HStringReference(RuntimeClass_Windows_UI_Input_RadialController).Get(),
    &controllerInterop);

// HWNDに対するIRadialControllerを取得する
Microsoft::WRL::ComPtr<ABI::Windows::UI::Input::IRadialController> controller;
controllerInterop->CreateForWindow(hwnd, IID_PPV_ARGS(&controller));

イベントハンドラの追加がめんどくさめなことを除けば、だいたい同じ感じでどうにかなります。

システム定義のアイコンを消したい

Surface Dialを長押ししたときにでるメニューは、デフォルトだとシステム定義のアイコンがいくつかならんでます。

https://i-msdn.sec.s-msft.com/en-us/windows/uwp/input-and-devices/images/windows-wheel/surface-dial-menu-offscreen.png

これを消したりするには、RadialControllerConfiguration::SetDefaultMenuItemsを呼び出すことでできます。 ちなみにMSDNに特に書いてないですが、カスタムアイコンを追加せずにシステムアイコンをすべて消そうとすると、呼び出しは失敗して システム設定された状態にリセットされます。どうやら仕様だそうです。

SetDefaultMenuItemsは、.NETの世界からだとIEnumerable<RadialControllerSystemMenuItemKind>を引数として取るので、 new List<RadialControllerSystemMenuItemKind>();とか適当にやればインスタンスが作れるのですが、C++だと微妙に話が違ってきます。

C++での引数はABI::Windows::Foundation::Collections::IVector<ABI::Windows::UI::Input::RadialControllerSystemMenuItemKind>*となっています。 ちなみに似たようなの3種類あります。

  1. ABI::Windows::Foundation::Collections::IVector
  2. Windows::Foundation::Collections::IVector
  3. Platform::Collections::Vector

似たようなのがいろいろあってめんどくさいですが、1はWinRT ABIでやり取りされる型のインターフェース。2はWinRT内部でやり取りされる型のインターフェース。3は2に対する実装。です。 2が指示されている場合は、3をインスタンス化して使えばOKですが、1が指示されているばあいは、対応する型を手書きする必要があるようです。*4

今回は1が指示されているので、適当に実装するなりなんなりして使えばおっけーです。

MIDLを要求される

ABI::Windows::Foundation::Collections::IVector<T> を実装したクラスを作った。Microsoft::WRL::Make<T>経由でインスタンスも作った。 これで完璧。とおもったらそうでもなく。ABI::Windows::Foundation::Collections配下のインターフェースに対応する実装はMIDLでの定義が必要です。 プロジェクトにIDLファイルを追加して、次のようなコードが必要です。この時、名前空間はIVectorの実装をインスタンス化する位置のものを指定します。

import "inspectable.idl";
import "Windows.Foundation.idl";
import "Windows.UI.Input.idl";

#define COMPONENT_VERSION 1.0

namespace DialWin32
{
    declare{
        interface Windows.Foundation.Collections.IVector<Windows.UI.Input.RadialControllerSystemMenuItemKind>;
    }
}

これをビルドすると、たぶん失敗します。プロジェクトを右クリックして、プロパティを開いて、MIDLの設定を少し変えます。

  • C/C++ -> General -> Additional Metadata Directories
    • C:\Program Files (x86)\Windows Kits\10\References\Windows.Foundation.FoundationContract\1.0.0.0*5
  • MIDL -> Advanced -> Prepend with ‘ABI’ namespace
    • Yes

これでビルドすると、IDLファイルのファイル名に _h.h を追加したファイルが生成されます。 このヘッダをincludeして、ABI::Windows::Foundation::Collections::IVector<T>の実装をMicrosoft::WRL::Make<T>で作ってあげれば やっとABI空間のIVector実装のインスタンスが手に入るわけです。

このあたりをやらないと

midlrt : error MIDL4034: Failed to load a dependency file. Windows.winmd (HRESULT:0x80070002 - The system cannot find the file specified. )

と言われたり、

error C2338: This interface instance has not been specialized by MIDL. This may be caused by forgetting a ‘*’ pointer on an interface type, by omitting a necessary ‘declare’ clause in your idl file, by forgetting to include one of the necessary MIDL generated headers.

と言われたりします。

それから、Prepend with 'ABI' namespaceを付けてないと、MIDLコンパイラが出力したヘッダファイルのコンパイルに失敗します。

おわりに

C++/Win32からSurface Dialを使うのは、IVectorさえ出会わなければそんなに大変じゃないです。IVectorもわかってしまえば大したことないです。 私のぐぐり力が低いばかりに、解決まで3日もかかってしまったのが非常にあれですが…

カスタムアイコンを追加するのはまだ試してないですが、たぶんWPFのつらいあれよりさらにつらそうだな…とおもいつつ、後日試してみます。

*1:http://blog.okazuki.jp/entry/2016/11/11/171706

*2:http://grabacr.net/archives/7141

*3:Windows classic samplesから引用 https://github.com/Microsoft/Windows-classic-samples/blob/master/Samples/RadialController/cs/winforms/RadialControllerInterop.cs

*4:Win2Dも手書きしてたのでたぶんそういうもんらしい

*5:たぶんもっとちゃんとした指定があるはずです…