trsing’s diary

勉強、読んだ本、仕事で調べたこととかのメモ。

XAMLでImageとMediaElementのSourceについて

なんかデザイナとImageMediaElementSourceの解釈違うようだったのでメモ

まとめ

Windows 10、VisualStudio2019、.Net Framework 4.7.2
画像、動画ともにビルドアクションはコンテンツで出力ディレクトリーにコピー

f:id:trsing:20210822160241p:plain
フォルダ構成

f:id:trsing:20210822163613p:plain
実行時の表示結果

f:id:trsing:20210822161734p:plain
Source。<!--O-->は表示された。<!--X-->は表示されなかった。

f:id:trsing:20210822162256p:plain
デザイナの表示

  • Image相対パス.xamlファイルが基準。/で始まる場合は実行ファイルが基準。 っぽい。
  • MediaElemen相対パスは実行ファイルが基準。/で始まる場合は(たぶん実行ファイルがある)ドライバのルートが基準。っぽい。
  • デザイナの場合Imageと同じ結果になったけどタイミングによって更新されたり更新されなかったり変な挙動してたのでよくわからん(めんどくさくなった)。プロジェクトに登録されたものを探す、なければディレクトリのルートを見に行くっぽい。

MSDN

ImageMediaElementSourceの説明が違うので仕様か。

Image

The source of the drawn image.

MediaElement

The URI that specifies the source of the element.

その他

Imageの場合プロジェクトに登録していない画像ファイルは実行時に表示されなかった(D:/~みたいに絶対パスで書いた場合は表示された)。MediaElementの場合は登録してなくても表示された。

ImageMediaElementは似たものではなく全く違ったもの、という気がしてきた。

あなたの5GにLAN直結

新型コロナウイルスワクチン予防接種(一回目)メモ

職域接種でモデルナ。念のため当日・翌日に有休とったけど打った左腕にちょっと違和感程度の軽い副反応で済んだっぽい@7/28 21:00。二回目もこの程度だといいな。

7/27

09:30 とりあえず体温を測る。37度(平熱)
10:00 会場で体温を測る(非接触)。36.5度。注射の痛み全くなし。チクリともせず。
10:15 15分の様子見終わり。左腕に違和感が全くないわけではない、程度
10:50 おうち。帰宅途中に買い物。きつめの副反応が出た場合に備えてポカリ、ウィダーx2、バッファリンプレミアム
14:20 2hの激闘の末義父を下す(SEKIRO)。ちょっと頭痛がするが集中してゲームしたせい。とりあえず体温測ってみると36.3度。下がっとる
15:51 ちょっと頭痛がきつくなってきたかも。頭痛持ちなのでいつものか副反応かわかんね。左腕は押したり伸ばしたりしたらちょっと痛い。腫れてはいない。36.6度
18:23 頭痛が続くがいつものか副反応なのかわからない。左腕は安静状態でも軽い痛み
20:19 腕を上にあげるとちょっと痛いけどまあ無視はできるかなーくらい
22:30 肩こりが酷くなってきたような気がするけど肩こりもいつもあるしな・・・
23:50 寝る前に体温測る。36.7度。頭痛は多少

7/28

08:00 起きた。左腕はちょっと動かしにくいが頭痛も肩こりもほぼない。もう気にしないでいいか。37.0度

C#でUIAutomationを使う

目的

UIAutomation使って自動でボタンポチポチ押すくらいできるようになる

対象

ちょっとUIAutomation使ってみたいなーくらいの人

ざっくりとした手順

  1. 操作したいオブジェクトを調査する
  2. 操作したいオブジェクトのAutomationElementを取得する
  3. 取得したオブジェクトを操作する

詳細

UIAutomation Treeについて

Desktopを根とした木構造になっている。

Desktop-アプリ-アプリが持つボタン、メニュー、etc.-…

これをたどって操作したいオブジェクトのAutomationElementを取得する。

操作したいオブジェクトを調査する

調査で取得する情報

オブジェクトの検索に使う情報を取得する。ひとまず Name,ControlType,AutomationId,ClassName を調べればよい。対象は操作したいオブジェクトとそれの親 or 祖先であるアプリ。

調査方法

以下のツールを使う。

UIAutomationSpy,Inspect,Accessibility Insights
など。

操作したいオブジェクトのAutomationElementを取得する1

まず検索の起点となるAutomationElementを取得する。 取得したAutomationElementから条件と方向を指定して検索する。

操作したいオブジェクトを持つアプリのAutomationElementを取得して検索の起点とするとよい。

アプリのAutomationElementを取得する方法

方法はいくつかあるがアプリはDesktopの子要素であることが多いので、DesktopAutomationElementから子要素を対象としてアプリを検索すると楽。

Desktopの子要素でnameと一致するNameプロパティを持つものを取得。

var cnd = new PropertyCondition(AutomationElement.NameProperty, name);
var apliElement = AutomationElement.RootElement.FindFirst(TreeScope.Children, cnd);

AutomationElement.RootElementDesktopAutomationElement

検索の詳細については次項。

検索方法

起点となるAutomationElementから検索条件と検索対象範囲を指定して検索する。

検索条件を作成する2

検索対象とするプロパティと一致条件を指定して検索条件を作成する。

検索対象をNameプロパティ、nameと一致することを条件とする場合。

var cnd = new PropertyCondition(AutomationElement.NameProperty, name);

AutomationElement.NamePropertyを変更すればControlType,AutomationId,ClassNameを対象にできる。 それぞれAutomationElement.ControlTypeProperty,AutomationElement.AutomationIdProperty,AutomationElement.ClassNamePropertyControlTypeを対象にする場合、一致条件にControlTypeクラスを指定する。他はstring

検索する

検索の起点から検索条件と検索対象範囲(自分、子要素、孫要素)を指定して検索する。

起点であるapliElementから条件cndで子要素を検索する場合。

var targetElement = apliElement.FindFirst(TreeScope.Children, cnd);

FindFirstの場合条件に合った最初のAutomationElementを返す。 FindAllを使うと条件に合ったすべてのAutomationElementを返す。

TreeScope.Childrenを変更することで検索対象を変更できる。

オブジェクトを操作する

取得したAutomationElementから適切なPattern Objectを得てそれを操作する。

適切なPattern Objectを取得する

操作に適切なAutomationPatternを指定してPattern Objectを取得する。

ボタンを押すのであればInvokePattern.Pattern

var btn = targetElement.GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;

GetCurrentPatternの返り値はobjectなので変換を忘れずに。

取得したPattern Objectを操作する

取得したPattern Objectのメソッドを実行する。 ボタンを押す場合はInvoke

btn.Invoke();

参考資料

AutomationElementを取得する方法

Obtaining UI Automation Elements - Win32 apps | Microsoft Docs

ControlTypeについて

ControlType Class (System.Windows.Automation) | Microsoft Docs

検索範囲(TreeScope)について3

TreeScope Enum (System.Windows.Automation) | Microsoft Docs

ControlTypePatternの対応表

Control Pattern Mapping for UI Automation Clients | Microsoft Docs

それぞれのPatter詳細へのリンク

UI Automation Control Patterns Overview | Microsoft Docs

その他

menuItemとか右クリックメニューとかを操作したい場合

目標とするオブジェクトが描画されてないと検索できないっぽい4
MenuItemを操作するには一旦Menuを取得して展開してから検索・取得・操作する。 右クリックメニューを操作するには右クリックイベントを発生させたりショートカットキーを使って表示させてから検索・取得・操作する。

ショートカットキーを使って右クリックメニューを表示させる例。右クリックメニューを持つオブジェクトにフォーカスしてからショートカットキーを送る。送った後ちょっと待ち時間を入れた方がよい。

automationElement.SetFocus();
SendKeys.SendWait("+{F10}");

  1. 例にあげているもの以外については参考資料を確認

  2. 有効なものとか複数条件とかいろいろできる

  3. Ancestors,Parentは Not supported であることに注意

  4. 最小化したら検索できなくなったりするのもある。何やねんこいつら。

艦これイベントスレから攻略編成書き込みの番号抽出

背景

攻略情報まとめてくれているいつもの人が忙しいらしいどうしよう困った。誰か・・・誰か助けてくれ

目的

艦これイベントスレから海域攻略編成書き込みの番号を抽出する

コード

import sys
import requests
from bs4 import BeautifulSoup
import re


def getHenseiNo(url):
    res = requests.get(url)
    soup = BeautifulSoup(res.text, 'html.parser')
    dts = soup.find_all('dt')  # 1:名無しの提督@~
    dds = soup.find_all('dd')  # 内容

    dct = dict()
    kaiikiPattern = r'【海域】.*(E-?\d-?\d?)'
    for dt, dd in zip(dts, dds):
        no = dt.find('a').get_text()
        for text in dd.get_text().split('\n'):
            ret = re.match(kaiikiPattern, text)
            if ret:
                kaiiki = ret.group(1)
                kaiiki = re.sub('E-', 'E', kaiiki)
                l = dct.get(kaiiki, [])
                l.append(no)
                dct[kaiiki] = l
                break
    return sorted(dct.items(), key=lambda x: x[0])


if __name__ == '__main__':
    ret = getHenseiNo(sys.argv[1])
    for k, n in ret:
        print(f'{k}\n>>{",".join(n)}')

使い方と出力

$ python printHenseiNo.py http://jbbs.shitaraba.net/bbs/read.cgi/netgame/12394/1620448639/
E1-1
>>18,20,49,60,61,70,82
E1-2
>>19,21,24,28,57,71,78,83,114
E1-3
>>22,23,35,42,45,59,72,79,84
E2
>>58
E2-1
>>36,37,48,51,55,74,80,81,98
E2-2
>>38,54,56,69,85,94,101,103,107
E2-3
>>40,88,108,109,115
E3
>>99,117,122,135
E3-1
>>53,62,104,116,118,132
E3-2
>>63,64,102,105,110,119,123,133
E3-3
>>67,86,89,90,91,97,106,111,112,113,120,136,138
E4-1
>>127,128,131,134,140

謝辞

情報書き込んだりまとめたりしてくれる人ありがとう。いつもお世話になっています。感謝。

その他

札おおすぎてわけわからないたすけて

WSL2でUbuntuでpyenv+pipenvでVSCode+Pythonでjupyter notebookっぽい開発環境構築

目標

  1. Windows上でUbuntuにpyenv+pipenvでPython環境を構築する
  2. 上記に加えVSCodeでJupyter notebookっぽく使えるようにする1

f:id:trsing:20210418174056p:plain
こんな感じ

手順

WSL2導入~VSCode導入まで

次の記事がわかりやすい

qiita.com

注意点

  • pyenv install X.X.Xをする前にliblzma-devを導入する(apt install liblzma-dev)。pandasを使う場合に必要2
  • 環境変数(PYENV_ROOT,PATH)は.bash_profileで設定する方が行儀が良いらしい3

残り作業

  1. 作業フォルダで仮想環境構築(pipenv shell/pipenv --python 3)
  2. ipykernelを導入(pipenv install ipykernel)
  3. VSCodeで作業フォルダを開く
  4. VSCodeinterpreterを設定する(必要なら。自分の場合は自動で設定してくれた)
  5. jupyterのinterpreterを設定する(必要なら。自分の場合は自動で設定してくれた)

あとは適当な感じでご自由に

f:id:trsing:20210418175941p:plain
VSCodeinterpreter設定入口

f:id:trsing:20210418180038p:plain
jupyterのinterpreter設定入口

注意点

  • VSCodeで作業フォルダ以外を開いた場合interperterの設定が面倒になる。pipenvがフォルダごとに仮想環境を作るみたいなのでそれはそう(あまりよくわかっていない)。
  • jupyterでなんかインストールしてねえぞエラーが出てもVSCode立ち上げなおしたりubuntuを再起動したりしたら出なくなったりした。エラー無視しても問題なかったりした。なんやねんこいつ。

その他メモ書き

作業用ユーザー作成、sudoグループに追加

ubuntuにrootでログインして次のコマンドを実行

adduser <username>
gpasswd -a <username> sudo

default userの切り替え

windowspowershellを立ち上げて次のコマンドを実行

ubuntu2004 config --default-user <username>

指定したユーザーでログイン

windowspowershellを立ち上げるて次のコマンドを実行

wsl -u <username>

ubuntuシャットダウン(他も全部巻き込み)

windowspowershellを立ち上げて次のコマンドを実行

wsl --shutdown

設定はできてるはずなのに動かない場合

とりあえずubuntu再起動だ

proxyの設定4

次の記事を参考にするとよい。

qiita.com

とりあえず.bashrc環境変数http_proxyhttps_proxyの設定をするようにした5

echo 'export http_proxy="http://xxx.xxx.xxx.xxx:8080"' >> .bashrc
echo 'export https_proxy="http://xxx.xxx.xxx.xxx:8080"' >> .bashrc

作業用ユーザーからsudo apt ~するのでapt.confの設定もしておいた。


  1. 参考

  2. Variable(DataFrame)Data Viewerで覗いたときにPandasがないよとエラーが出た。ググっても何もわからん……してるうちにさらに壊れたのかimport pandasだけでエラーが出るようになったたため原因に気づいた。参考

  3. .bash_profaileはログイン時、.bashrcはシェル立ち上げごとに実行されるため(ログイン時にも実行される)

  4. 会社でなんかしようとすると毎回はまる…

  5. 追記(>>)じゃなくて上書き(>)しちゃってあわあわしてた。慌てず騒がず落ち着いて元ファイルからコピーすれば良い(cp /etc/skel/.bashrc ~)。参考

WindowsでサービスからGUIを立ち上げる

下記リンクを辿れば必要なことはだいたい記述されている。

stackoverflow.com

なので以下メモ書き。

前提知識

セッションに関する知識があれば何をやっているか理解しやすい。 次の資料がわかりやすかった。

www.mbsd.jp

ざっくりと書くと

  • サービスにはSessionId0が割り当てられる
  • ユーザごとに個別のSessionId(1以上)が割り当てられる1
  • SessionはWindowStationを含み、WindowStationはDesktopを含む
  • ユーザが普段目にするのは自分のセッションのDesktop

説明

https://stackoverflow.com/a/24122826 であげられているソースについて

サービスからGUIを立ち上げるにはログオンしているユーザのセッションのdesktopに対してプロセスを起動できれば良い。

実際、このコードでやってるのは

  1. ログオンユーザのセッションIDを取得
  2. 取得したセッションIDからトークンを取得
  3. WindowStationとDesktopを指定してプロセスをスタート

ちょいと細かく書くと

  • l.174~l.195:アクティブユーザのsessionを取得
    WTSEnumerateSessions:Remote Desktop Session Host server上のsessionリストを取得

  • l.197:user token取得
    WTSQueryUserToken:session IDからprimary access tokenを取得

  • l.200:user tokenを複製2
    DuplicateTokenEx:tokenを複製する。primary token/impersonation tokenを作ることができる。

  • l.229:デフォルトデスクトップを表示先に指定
    startInfo.lpDesktop = "winsta0\\default";

  • l.231:ユーザの環境変数を取得
    CreateEnvironmentBlock:指定したユーザの環境変数を取得

  • l.236:指定したトークンでプロセスを立ち上げ
    CreateProcessAsUser:tokenで指定されたユーザのsecurity contextでプロセスを立ち上げる。

なおこのサービスはLocalSystem accountで動かすことが前提のもよう。

WTSQueryUserToken の説明

To call this function successfully, the calling application must be running within the context of the LocalSystem account and have the SE_TCB_NAME privilege.

https://docs.microsoft.com/ja-jp/windows/win32/api/wtsapi32/nf-wtsapi32-wtsqueryusertoken

LocalSystem account以外でサービスを実行する場合

やることはたいして変わらない

  1. サービスを実行するユーザーにCreateProcessAsUserを実行するための権限を与える3
  2. ログオンユーザのプロセスを取得
  3. プロセスからトークンを取得
  4. WindowStationとDesktopを指定してプロセスをスタート

サービスを実行するユーザーとログオンユーザが同じ場合

動作環境

Windows 10 Pro、バージョン20H2、OSビルド19042.867
.Net Framework 4.7.2

ソースコード

GetSessionUserTokenの部分を次のようにすれば良い4

    foreach (System.Diagnostics.Process p in System.Diagnostics.Process.GetProcesses())
    {
        if (p.ProcessName.Equals("explorer"))
            explorerHandle = p.Handle;
    }
    if (OpenProcessToken(explorerHandle, TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY, out var hImpersonationToken))
    {
        var bResult = DuplicateTokenEx(hImpersonationToken, 0, IntPtr.Zero,
                (int)SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, (int)TOKEN_TYPE.TokenPrimary,
                ref phUserToken);
        CloseHandle(hImpersonationToken);
    }

stackoverflowには

The problem with Shrike's answer is that it does not work with a user connected over RDP.

とあるが手元の環境ではリモートデスクトップ経由で問題なく動作した。Windows10だとうまくいく?ユーザー一つしかないから?

  • remote desktop経由でセッションを確認
C:\Users\******>query session
 セッション名      ユーザー名               ID  状態    種類        デバイス
 services                                  0  Disc
>rdp-tcp#1         ******                 14  Active
 console                                  15  Conn
  • ローカルでセッションを確認
C:\Users\******>query session
 セッション名      ユーザー名               ID  状態    種類        デバイス
 services                                  0  Disc
>console           ******                 14  Active

サービスを実行するユーザーとログオンユーザが異なる場合

上記コードではうまくいかなかった。権限周りでの設定がなんやかんやいりそう。


  1. XP以前は最初にログオンしたユーザにSessionId0が割り当てられた。サービスとユーザを同じセッションにするとセキュリティ上問題があるのでVista以降は分けられたとか。調べてみると面白い。

  2. WTSQueryUserTokenでprimary access tokenが得られるので動かすだけなら不要。とはいえ複製しといた方が無難。

  3. ローカルセキュリティポリシーでローカルポリシー->ユーザー権利の割り当て->プロセスレベルトークンの置き換え。「プロセスレベルトークンの置き換え」の説明:このセキュリティ設定は、あるサービスから別のサービスを開始するために、CreateProcessAsUser() アプリケーション プログラミング インターフェイス (API) を呼び出すことができるユーザー アカウントを決定します。

  4. DuplicateTokenExはなくても動きはする。

Storyboardを作成する(Code Behind)

目次

  1. WPFのCustomControlでアニメーション-概要
  2. LineにLinearGradientBrushを適用したものをCustomControlとして作る
  3. Storyboardを作成する(XAML)
  4. Storyboardを切り替える(XAML)
  5. Storyboardを切り替える(Code Behind)
  6. Storyboardを作成する(Code Behind)←ここ

目的

Code BehindでStoryboardを作成する

作成

.cs

  • Storyboardを作成する
        private void UpdateStoryboard()
        {
            if (!(GetLineObject() is Line line)) return;
            gradientAnimationTrue?.Remove(line);
            gradientAnimationFalse?.Remove(line);
            if (AnimationType) gradientAnimationTrue?.Begin(line, true);
            else gradientAnimationFalse?.Begin(line, true);
        }

        private Storyboard gradientAnimationTrue = default;
        private Storyboard gradientAnimationFalse = default;
        private void CreateStoryboard()
        {
            var line = GetLineObject();
            var targetOffset = "(Line.Stroke).(LinearGradientBrush.GradientStops)[{0}].(GradientStop.Offset)";
            var duration = new Duration(TimeSpan.FromSeconds(1));

            var dat = new DoubleAnimation();
            dat.Duration = duration;
            dat.From = 0;
            dat.By = 1;
            Storyboard.SetTarget(line, dat);
            Storyboard.SetTargetProperty(dat, new PropertyPath(string.Format(targetOffset, 1)));
            gradientAnimationTrue = new Storyboard() { AutoReverse = true, RepeatBehavior = RepeatBehavior.Forever };
            gradientAnimationTrue.Children.Add(dat);

            var daf0 = new DoubleAnimation(0,1,duration);
            Storyboard.SetTarget(line, daf0);
            Storyboard.SetTargetProperty(daf0, new PropertyPath(string.Format(targetOffset, 0)));
            var daf1 = new DoubleAnimation(0, 1, duration);
            Storyboard.SetTarget(line, daf1);
            Storyboard.SetTargetProperty(daf1, new PropertyPath(string.Format(targetOffset, 1)));
            gradientAnimationFalse = new Storyboard() { AutoReverse = true, RepeatBehavior = RepeatBehavior.Forever };
            gradientAnimationFalse.Children.Add(daf0);
            gradientAnimationFalse.Children.Add(daf1);
        }
        private Line GetLineObject() => this.GetTemplateChild("gradientLine") as Line;
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            CreateStoryboard();
            UpdateStoryboard();
        }

Generic.xaml

  • Storyboardを削除する
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:GradientLineTest.Controls"
    >

    <Style TargetType="{x:Type local:GradientLineControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:GradientLineControl}">
                    <Line x:Name="gradientLine"
                          X1="{TemplateBinding X1}" Y1="{TemplateBinding Y1}"
                          X2="{TemplateBinding X2}" Y2="{TemplateBinding Y2}"
                          StrokeThickness="10"
                          >
                        <Line.Stroke>
                            <LinearGradientBrush StartPoint="0.0,0.5" EndPoint="1.0,0.5">
                                <GradientStop Color="{Binding LeftColor,RelativeSource={RelativeSource TemplatedParent}}" Offset="0.0"/>
                                <GradientStop Color="{Binding RightColor,RelativeSource={RelativeSource TemplatedParent}}" Offset="1"/>
                            </LinearGradientBrush>
                        </Line.Stroke>
                    </Line>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

注意点

SetTargetで指定できるのはFrameworkElement/FrameworkContentElementっぽい?(line.strokeとかを指定してもエラーは出ないけどアニメーションが動作しなかった)

SetTargetNameではFrameworkElement, FrameworkContentElement, Freezableを指定できる。

その他

targetpropertyについて

省略する場合

var targetOffset = "Stroke.GradientStops[{0}].Offset";

PropertyPath(String, Object[])使う場合

            var targetPatht = "(0).(1)[{0}].(2)";
            var dps = new DependencyProperty[]{
                Line.StrokeProperty,
                LinearGradientBrush.GradientStopsProperty,
                GradientStop.OffsetProperty
            };
            Storyboard.SetTargetProperty(dat, new PropertyPath(string.Format(targetPatht, 1), dps));

targetについて

Storyboard.SetTargetName(dat, "gradientLine");でもよい。 他適当に名前を付けてれば適当に使える。

    <GradientStop x:Name="gs1" Color="{Binding RightColor,RelativeSource={RelativeSource TemplatedParent}}" Offset="1"/>

とすれば

    Storyboard.SetTargetName(dat, "gs1");
    Storyboard.SetTargetProperty(dat, new PropertyPath(GradientStop.OffsetProperty));

とできる。

参考

The object must be a FrameworkElement, FrameworkContentElement, or Freezable.

https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.animation.storyboard.targetname?view=net-5.0

おまけ

LinearGradientBrushもCode Behindで作れば柔軟な操作ができる。線長に合わせてGradientStopを増やすとか。

        private void CreateLinearGradientBrush()
        {
            var gradientStops = new GradientStopCollection()
            {
                new GradientStop(LeftColor,0),
                new GradientStop(RightColor,1),
            };
            GetLineObject().Stroke = new LinearGradientBrush(gradientStops);
        }
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            CreateLinearGradientBrush();
            CreateStoryboard();
            UpdateStoryboard();
        }

作成したオブジェクトはthis.RegisterNameで登録しておくと便利。 ただしNameScope.SetNameScope(this,new NameScope());で独自のNameScopeを作成しておくこと。 作成しておかないとCustomControlを複数配置した場合、名前が被ってエラーになる。これはrootのNamescopeに登録されるため。たぶん。