C++ ときどき ごはん、わりとてぃーぶれいく☆

USAGI.NETWORKのなかのひとのブログ。主にC++。

XAML: 同じ ItemsSource を持つ複数のコントロールの効率的な定義の仕方、あるいは ItemsSource を StaticResource で別定義する方法

例えば同じ要素軍を選択肢として提示する複数のコンボボックスを配置したいとしよう。以下のコード例は何れも <Window> の内部に定義している。

<!--ダサいがシンプルで間違いは無い実装。しかしダサい。-->
<ComboBox Name="CB1">
  <ComboBoxItem Content="選択肢1"/>
  <ComboBoxItem Content="選択肢2"/>
  <ComboBoxItem Content="選択肢3"/>
</ComboBox>
<ComboBox Name="CB2">
  <ComboBoxItem Content="選択肢1"/>
  <ComboBoxItem Content="選択肢2"/>
  <ComboBoxItem Content="選択肢3"/>
</ComboBox>

↑ダサい。

そこで XAML 初心者のわたしは先ず次のようにしてみた。

<!--これは期待動作しません。-->
<ComboBox Name="CB1">
  <ComboBoxItem Content="選択肢1"/>
  <ComboBoxItem Content="選択肢2"/>
  <ComboBoxItem Content="選択肢3"/>
</ComboBox>
<ComboBox Name="CB2" ItemsSource="{Binding ItemsSource,ElementName=CB1"/>

↑これはコンパイルはできるが実行してもCB2の選択肢が何も出てこない。

<!--これも期待動作しません。-->
<ComboBox Name="CB1">
  <ComboBoxItem Content="選択肢1"/>
  <ComboBoxItem Content="選択肢2"/>
  <ComboBoxItem Content="選択肢3"/>
</ComboBox>
<ComboBox Name="CB2" ItemsSource="{Binding Items,ElementName=CB1"/>

↑これはコンパイルはできるし、実行するとCB2にも選択肢がでるのだが、CB2でドロップダウンされた選択肢を選択してもCB1の値へ適用されるという怪奇現象を引き起こした。

<Window
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

↑と x が使用可能として、

<!--期待動作する例-->
<Window.Resources>
  <x:Array Type="ComboBoxItem" x:Key="CBItems">
    <ComboBoxItem Content="選択肢1"/>
    <ComboBoxItem Content="選択肢2"/>
    <ComboBoxItem Content="選択肢3"/>
  </x:Array>
</Window.Resources>
<ComboBox Name="CB1" ItemsSource="{StaticResource CBItems}"/>
<ComboBox Name="CB2" ItemsSource="{StaticResource CBItems}"/>

↑期待動作する😃

ちなみに、 <ComboBoxItem Content="hoge" Tag="fuga"/> など Content 属性の他にも設定したい何かしらがある場合はさておき、そうではないただの文字列のリストの StaticResource を定義して使えればよい場合には、

<Window.Resources>
  <x:Array xmlns:s="clr-namespace:System;assembly=mscorlib" Type="s:String" x:Key="StringItems">
    <s:String>梅干し</s:String>
    <s:String>生姜</s:String>
    <s:String>小葱</s:String>
  </x:Array>
</Window.Resources>

↑など定義し、これも ComboBoxItemsSourceStaticRerouce としてぶちこめる。この場合には ComboBox ではなく ListBox にも放り込めるようになる。

なお、 XAML で完結する必要が無ければ XAML 側では ItemsSource{Binding MyDataContextProperty} としておいてコードで WindowDataContextIEnumerable< string >MyDataContextProperty を持つオブジェクトを放り込めばよい。この方法は ItemsSource に列挙する選択肢が実行中に変化する場合にはより適切だろう。

C#/JSON: Newtonsoft.Json の最も簡単な使い方

C#er には標準ライブラリーに準じてお馴染みらしい JSON を扱うライブラリー Newtonsoft.Json があるようだ。しかし、こういってはなんだがろくな解説が無い(個人の感想です。)ので私が使うにあたり最低限調査したメモを残す。

使用する主な名前空間は2つ。Linqusing しないと一見 LINQ 的には使わない場合にも面倒が起こる。具体的にはこのあとすぐに実装例に使う JTokenLinq 名前空間に所属している。

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

こうしたライブラリーの具体的な使用例は主には構造化されたデータの JSON 化と言語ネイティブデータ構造への戻しだろう。Newtonsoft.JsonC# で使う場合それは非常に簡単に実装できる。

class MyDataType
{
  public int Nyanko { get; set; }
  public string Wanko { get; set; }
}

void Experiment()
{
  // 言語ネイティブなユーザー定義型 -> JSON オブジェクト
  var data = new MyDataType() { Nyanko = 123, Wanko = "woowoo" };
  var json_object = JToken.FromObject( data );
  // JSON オブジェクト -> JSON 文字列
  var json_string = json_object.ToString();
  // JSON 文字列 -> 言語ネイティブなユーザー定義型
  var data2 = JsonConvert.DeserializeObject< MyDataType >( json_string );
}

型がどうで文字列にするとどうで、そういう事はデバッガーやインテリセンスで見れば良い事なのでこのメモでは解説を省く。

この方法の優秀な点として、

  1. ユーザー定義型やそのプロパティーに特別な属性を追加する必要は無い事。(器用な事をやりたければ専用の属性を追加する方法もある)
  2. JToken.FromObject / JsonConvert.DeserializeObject< T >IEnumerableDictionary を自動的に扱ってくれる事。

が挙げられる。

例えば、 WPFListView コントロールのデータの実体としてしばしば扱う事になる System.Collections.ObjectModel 名前空間ObservableCollection 型があるが、これの扱いも特別に気にする必要無く Newtonsoft.JsonJSON との相互変換を行ってくれる。

var data = new ObservableCollection< MyDataType >();
data.Add( new MyDataType() { Nyanko = 1, Wanko = "wanwan" } );
data.Add( new MyDataType() { Nyanko = 3, Wanko = "qwwwn" } );
var json_object = JToken.FromObject( data );
Console.WriteLine( json_object );

このような応用の場合にも特別な事は不要で JSON 化できる。以下はこの結果得られる JSON:

[
  {
    "Nyanko": 1,
    "Wanko": "wanwan"
  },
  {
    "Nyanko": 3,
    "Wanko": "qwwwn"
  }
]

C# ネイティブのオブジェクトへ戻す場合も特別な事は不要:

// 既にある ObservableCollection< MyDataType > のオブジェクトへ直接戻せる
data.Clear();
data = JsonConvert.DeserializeObject< ObservableCollection< MyDataType >>( json_object.ToString() );
foreach ( var datum in data )
  Console.WriteLine( "N={0} W={1}", datum.Nyanko, datum.Wanko );
N=1 W=wanwan
N=3 W=qwwwn

たいへん便利😃

大雑把にはこれだけで十分に使えるが JValue JObject JArray の基本的な構成と取り扱い、ライブラリーの全貌、詳細については

など眺めるとよい。

たいへん有名なライブラリーらしいのでググって解説記事でも見たほうが早いだろうと私も使い始めに思ったのだけど、どうもインチキな解説やわかっていない解説が上位に並び時間を無駄にした。今回メモに残した大雑把な使い方と公式 API Reference をながめるのがよい。

Windows: スタートボタンが死んだ日

Note: 今回はわりとエッセイ的ななかみです。

もう数年程度は必要の無かったので Windows 10 は(昔に比べれば)安定動作してくれてその点では余計な手間が少なくなって嬉しいな、と思っていた。

だが、今日は スタートボタン が死んだ😂 タスクバーの他の機能は生きたまま、スタートボタンだけ無反応になった。 Meta ( Windows ) キーも CTRL+ESC も、ポインティングデバイスの左ボタンにも無反応だった。ポインティングデバイスの右ボタンには反応するが、それは別の機能(設定などのメニュー)であり、 Windows ユーザーに愛され続けているスタートボタンのそれではない。

ともあれ、こんな事もあろうかと、というわけではないのだけど、私はタスクバーに Task Manager のショートカットを置いているし、 NT 時代から伝統の CTRL+ALT+DEL からでも失敗することなく Task Manager を動かせる。

さて、 Windows 10 ではたぶん初めてこの状況に対する復帰テクニックを試す事になるのだけど期待動作してくれるだろうか。「スタートボタンだけが死んだ」、そんな状況からソフトウェアエンジニアとしての私の脳は「もしかしたらプロセスレベルでタスクバー、スタートボタン、あるいはエクスプローラーは既に分離されていてこの方法は無駄になったのかもしれない」なんて事を考えていた。

f:id:USAGI-WRP:20181206124547p:plain

先ず、 Processes タグの Name から目的のそれを探す。Name の表示も分類ごとに分けられたいへん扱いやすくなった。 Apps, Background processes, それから Windows processesWindows processes に分類された中に Windows Explorer が居る。 Windows Explorer が見つかった事で「昔からの方法はまだそのまま使えるかもしれない」と少し安堵した。

f:id:USAGI-WRP:20181206125344p:plain

何かと便利になった Task Manager にはプロセスの Restart 機能もアクセス良く提供されるようになった。

どうやらこの方法は現在の Windows 10 でも "正解" らしい。タスクバー、というよりデスクトップがさっくりと再起動され、すぐに スタートボタン も期待動作するようになった。

だそく

なんでこんな話をわざわざ書いたのか。

どうも巷には "この程度で済む事" へ powershell から dism やら sfc やら Get-AppXPackage に呪文を打ち込めやら時間も手間も(少なくともそうしたシステム管理を日常で必要としない一般的なユーザーにとっては)尋常ではなくかかり、何をやっているのか意味もわからないであろう方法を「スタートボタン 死んだ」など日本語で Google センセイから検索してみるとわらわらと上位に出てくる。

実際そういう必要のある状態になった例やひともいらっしゃるのかもしれないが…正直なところ「アホカ…」と思ってしまった。この記事で書いた方法でまずはたいていどうにかなるし、原因不明で何度も再発ということも一般的にはそうそうないのだから…(少なくとも多くのユーザーたちには)。

.net/XAML: bool を反転して Binding したかっただけなんだ

XAML で UI を作っていたんだ。

<!-- こういう事したかっただけなんだ -->
<CheckBox IsChecked="{Binding Awabi}"/>
<CheckBox IsEnabled="{Binding !Awabi}"/>

先日の 「.net/XAML/C#: XAML のプロパティーは Visibility 型だけど Boolean の DataContext に束縛したい場合の最適解、あるいは BooleanToVisibilityConverter について」 の例から IValueConverter を実装した InverseBooleanConverter 作ればできるのはわかる。わかるんじゃが…ここだけの為にそんな実装書きたくないでござる、とか思って boolNot くらいなんとかならんのかと stackoverflow を眺めたわけですよ。

 [ValueConversion(typeof(bool?), typeof(bool))]
public class InverseBooleanConverter : IValueConverter
{
    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (targetType != typeof(bool?))
        {
            throw new InvalidOperationException("The target must be a nullable boolean");
        }
        bool? b = (bool?)value;
        return b.HasValue && !b.Value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return !(value as bool?);
    }

    #endregion
}

はぁーーーーー書くのめんどくさいでござるぅーーー(コピペするだけなんだけど「たったの ! したいだけ」のためにこんなにプロジェクトにソースコード増やしたくないでござるぅー…)

と、思って、もうちょっと良い方法かつトリッキーじゃなくて DataContext に放り込む値は bool そのままで使える方法は…とスクローリングしたわけですよ。

<Button IsEnabled="{c:Binding Path=!IsReadOnly}" />
<Button Content="{c:Binding ElementName=grid, Path=ActualWidth+Height}"/>
<Label Content="{c:Binding A+B+C }" />
<Button Visibility="{c:Binding IsChecked, FalseToVisibility=Hidden}" />

『あなたが(XAMLの)神か🙏』

この神 Answer をしてくれっていた Alex141 はこのライブラリー Alex141/CalcBinding の Author さんのようだ。プロジェクトの References へ Manage NuGet Packages から CalcBinding を install して XAMLWindow に参照を追加するだけで思い描いた理想の ! がほんとうに使えるようになってしまう。XAMLの真髄をすすってる感があってたいへんXAMLだ。

<Window 
  xmlns:c="clr-namespace:CalcBinding;assembly=CalcBinding"
<!-- こういう事したかっただけなんだ
  <CheckBox IsChecked="{Binding Awabi}"/>
  <CheckBox IsEnabled="{Binding !Awabi}"/>
 -->
<!-- かなり理想郷に近いぞ!!!😂 -->
  <CheckBox IsChecked="{Binding Awabi}"/>
  <CheckBox IsEnabled="{c:Binding !Awabi}"/>

🙏🙏🙏🙏🙏

UE4/C++/BP: UGameplayStatics::OpenLevel の Options が ? に化ける怪異、あるいは UEngine::SetClientTravel のパラメーターの本当の意味について

次のコードあるいは相当のBPは期待とは異なる動作を招くかもしれない。

UGameplayStatics::OpenLevel
( GetWorld()
, TEXT( "Sushi" )
, true
, TEXT( "#Tako" )
);

『このようなレベルの開き方をした場合、開かれたレベルでは AGameMode::OptionsString? に「化け」たように観測される現象が必ず発生する( ・`ω・´)』

ΩΩΩ<な、なんだってー!?

聡明な UE4/C++er の各位は、既に疑いの眼差しを Options として与えられる実引数が Tako ではなく #Tako とされている点へ向いていると思う。その通りだ。私がそれに気付くためにはユーザーが与えたパラメーターの収集とパターンの分析、 UGameplayStatics の実装詳細の確認、加えて UEngine::SetClientTravel の実装詳細を確認する必要があった。冒頭のコード例は原因が分かりやすい例に簡略化した後のものだ。

先ず、この現象はバグではない。 UE4 の設計に基づいた正しい挙動である事を明示しておく。 UE4OpenLevel には # 文字を含めてしまうとプログラマーあるいはユーザーが意図したであろう Options は開かれるレベルの AGameMode::OptionsString へ伝達されない。それはなぜか?

UGameplayStatics::OpenLevel を眺めてみよう。

この部分の実装詳細を見るとパラメーターの無駄な操作が気になるかもしれないが、やっている事はパラメーターの簡単な確認と enum への変換を行っているがこの現象を引き起こす原因は無く、最終的に UEngine::SetClientTravel を呼び出す実装になっている。

// Note: 要点だけにした擬似コード。あくまでも擬似コード注意。
void UGameplayStatics::OpenLevel(const UObject* WorldContextObject, FName LevelName, bool bAbsolute, FString Options)
{
  FString Cmd = FString( LevelName ) + TEXT( "?" ) + Options;
  const ETravelType TravelType = bAsbolute ? TRAVEL_Absolute : TRAVEL_Relative
  GEngine->SetClientTravel( World, *Cmd, TravelType );
}

UEngine::SetClientTravel の呼び出し時点では「化け」や「欠損」は起きていないが、聡明な UE4/C++er たちはちょっとした引っ掛かり、気付きのようなものを得たかもしれない。

  1. 「文字列で結合した」
  2. 「レベル名文字列とオプション群文字列の間に ? 文字をセパレーターとして挿入した」

その通りだ。問題の核心にだいぶ接近してきた。

続いて、 UEngine::SetClientTravel を見よう。

この関数は短く簡単だが、問題の本質に迫るヒントが大きい。

void UEngine::SetClientTravel( UPendingNetGame *PendingNetGame, const TCHAR* NextURL, ETravelType InTravelType )
{
    FWorldContext &Context = GetWorldContextFromPendingNetGameChecked(PendingNetGame);

    // set TravelURL.  Will be processed safely on the next tick in UGameEngine::Tick().
    Context.TravelURL    = NextURL;
    Context.TravelType   = InTravelType;

    // Prevent crashing the game by attempting to connect to own listen server
    if ( Context.LastURL.HasOption(TEXT("Listen")) )
    {
        Context.LastURL.RemoveOption(TEXT("Listen"));
    }
}

呼び出し元で Cmd として結合されていたパラメーターは NextURL 仮引数に渡っている。名前でもうわかったかもしれない。

UE4 のレベル間の遷移パラメーターは簡単には「レベル名文字列」と「オプション群文字列」にある意味で "偽装" されているが、実際には「URL」だったんだ( ・`ω・´)』

ΩΩΩ<な、なんだってー!?

一般的に URL のパターンを構成要素ごとの部品に分解すると、

// Note: 厳密にはちょっと違いますが…
{scheme}://{host}:{port}/{path}?{query}#{fragment}`
// scheme <- http とか ftp とか
// host <- 127.0.0.1 とか www.example.com とか
// port <- 80 とか 443 とか
// path <- /ja/docs/hoge とか
// query <- ?q=sushi&action=eat とか
// fragment <- #top とか

と、いうわけで、そもそも UE4 のレベル間の遷移パラメーターは URL 形式だったので # 文字があると「フラグメント」扱いで対象ホストには渡らないのでした。( URL 的にはフラグメント部分は呼び出し元にしか影響せず呼び出し先へは伝達しない)

おまけ

WorldContext::TravelURL も覗いてみよう。

 /** URL to travel to for pending client connect */
    FString TravelURL;

と、いうわけでここへ文字列値をセットしただけでは # 以降のフラグメント文字列の消失事件はまだ起っていない。その後 TravelURL が参照される際に FURL を介して ?# で分割、フィルターされフラグメント扱いの # 以降が消失して AGameMode::Init へ渡されているものと想像できた。冒頭の「消失」ではなく「化け」も UE4 が設計上 URL として予期していない Sushi?#Tako が与えられた事でオプション文字列の実装都合 ? 文字がオプション群文字列として渡ってしまったのだろう。

UEngine::Browse あたりを眺めると、どうやらそもそもレベルを開く際に渡す Options はユーザーが任意に与えて良い設計でもなかったようだ(事実上そうそうクエリーが意図せず UE4 で予約されたクエリー名と被ることもないのだろうけれど)。

ちょっとした文字列パラメーターをレベル間で渡すのに便利なのでついこの程度の実装詳細も追わないまま、ただの文字列パラメーターを渡せる機能だろうと使ってしまっていたが、レベル間のユーザーデータ、パラメーターの伝搬には別の手段、素直な選択肢としてはレベル間で値を伝搬する仕組みを実装したカスタム UGameInstance を使うとか、外部のストレージを経由させるとかするのが良さそうだ。

今回はこのあたりまで確認した段階で "探索" は終える事にした。

ちなみに、 FURLSushi?#Tako を与えると、

FString s( TEXT( "Sushi?#Tako" ) );
FURL url( *s );
UE_LOG( LogTemp, Log, TEXT( "Protocol=%s Map=%s Op.Num()=%d" ), *url.Protocol, *url.Map, url.Op.Num() )
Protocol=unreal Map=Sushi?#Tako Op.Num()=0

こんな結果が得られる。どうやら、この段階でフラグメントが消失したり化けたりしているわけではなかったらしい。 UEngineAGameMode か、これ以上を知りたい場合はソースコードのリーディングではなくエンジンにデバッガーを挿した方が手っ取り早そうだ。

UE4/C++: UEDirectInputPadPlugin を Fork してにゃんにゃん

katze_7514 さんの UEDirectInputPadPlugin を Fork して でにゃんにゃんしました。

UE4 には 4.15 以降は標準で Windows RawInput Plugin が入りましたが、

  • 軸の入力値が unorm でそのまま Input のマッピングへ入力されてしまうので XInput と同じマッパーにできず XInput とは別に RawInput 用のマッピングを用意する必要がある。(あるいは Product ID, Vendor ID からデバイスごとに値域のマッパーを定義するか…)
  • ボタンのイベントが安定しない。(連打しても数秒に1度しか Pressed が来ないとか…)

など、既に UEDirectInputPadPlugin でちょろっとマッピングして便利に XInput / DirectInput の両対応をしているプロジェクト、あるいはその経験があるといまひとつ現時点の Windows RawInput Plugin に変更する気が起こらないのです。

Fork してにゃんにゃんした最初の理由はこちら:

無限ループになっちゃうパターンでアプリが永遠に起動しない現象がたまに観測されちゃった事がきっかけ、ついでにソース中の Input() が成功するケースでも最小で 200 ms × 10 回 = 2秒ちょいを起動時にここで待たされてしまう実装だったので15年くらい昔に DirectInput の実装書いて爆発していた頃の記憶も思い出しながらちょいちょいコレデイイハズ・サイテキカ(公式に基づく仕様があるわけじゃないのだけど…)しといた、と。

コレデイイハズ・サイテキカだったり、 BT 接続の DirectInput の不安定性だったり、 Fork 元にばよえ~んするのもどうかなーと思ったので Fork してひとりにゃんにゃんしたのでした。このあとも何かいじるかは、また何か不便が起きるまでないかも?あるかも?😃

.net/XAML/C#: XAML のプロパティーは Visibility 型だけど Boolean の DataContext に束縛したい場合の最適解、あるいは BooleanToVisibilityConverter について

Boolean なプロパティー HogeDataContext に束縛された Window のにある何かのコントロール(下の例では Button )の Visibility プロパティーXAML だけで簡単に WindowDataContextHoge に束縛したい!」と思ったんだ。

<Window ... 略 ...>
  ... 略 ...
  <Button Visibility="{Binding 😂 Hoge ? Visible : Hidden したいんだけど!!😂}"/>
  ... 略 ...
</Window>

『わからん。XAML なーんもワカラン… (けど何か良い方法はあるじゃろう… ヘ(゚∀゚ヘ) )』

と、思ったのだけど XAML 関連の知識が乏しいので msdn や stackoverflow を眺めるにも答えに近そうなキーワードがわからないのでもぞもぞ XAML 関連のインターフェースやら Binding やら Command やらを地味に読んで…いたのだけど、インターフェースの定義やら dynamic な暗黒呪文の把握やら面倒くさそーなコトが必要ならプログラマー・スナオビリティー的に ctor &状態変更が必要になるタイミングイベントに Visibility プロパティーを更新する .cs 実装を書いた方がはやくて楽なんでぃゎ…とか思い始めたのね。

そこで、チョット諦めてそういう実装にしようかと思いながらも…

などと他力本願。そしたらとっちゃんさんがまさに求めていたそれ的な最適解を教えてくれました😃

つまり…

<Window ... 略 ...>
  <Window.Resources>
    <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
  </Window.Resources>
  ... 略 ...
  <Button Visibility="{Binding Hoge,Converter={StaticResource BooleanToVisibilityConverter}}"/>
  ... 略 ...
</Window>

これだけで良かったんだね😂 カンタン、書く量も必要最低限で済んでますって感じ。とっても最適解♥

おまけ: その後の会話の続き

ありがとうございました😃