Direct2D導入(ID2D1HwndRenderTarget)
今回はDirect2Dを用いて、下記のような描画を行うところまで実装してみたいと思います。
ウィンドウ生成、表示関数
Direct2Dの描画先として、通常のWindowsのウィンドウを作成、実行する関数を準備します。
内容は一般的なもので、特に変わったものではありません。
HWND CreateHWND(WNDPROC wndProc) { WNDCLASSEX wndclass; wndclass.cbSize = sizeof(WNDCLASSEX); wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS; wndclass.lpfnWndProc = wndProc; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hInstance = GetModuleHandle(NULL); wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground = NULL; wndclass.lpszMenuName = NULL; wndclass.lpszClassName = L"Main Window"; wndclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION); RegisterClassEx(&wndclass); HWND hwnd = CreateWindow( L"Main Window", L"Main Window", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, wndclass.hInstance, NULL ); return hwnd; }
void Run(HWND hwnd) { ShowWindow(hwnd, SW_SHOWNORMAL); MSG msg; while (GetMessage(&msg, nullptr, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } }
Direct2D初期化
ID2D1HwndRenderTargetを生成する関数を作成します。
ID2D1Factory経由で作成を行います。
#include <d2d1.h> #pragma comment(lib, "d2d1.lib") #include <wrl\client.h> using namespace Microsoft::WRL; ComPtr<ID2D1HwndRenderTarget> CreateRenderTarget(HWND hwnd) { ComPtr<ID2D1Factory> factory; AssertHR(D2D1CreateFactory<ID2D1Factory>(D2D1_FACTORY_TYPE_SINGLE_THREADED, &factory)); ComPtr<ID2D1HwndRenderTarget> renderTarget; AssertHR(factory->CreateHwndRenderTarget( D2D1::RenderTargetProperties(), D2D1::HwndRenderTargetProperties(hwnd), &renderTarget )); return renderTarget; }
楕円描画
RenderTargetいっぱいに楕円を描画する関数となります。
void Draw(ID2D1HwndRenderTarget* renderTarget) { renderTarget->BeginDraw(); renderTarget->Clear(D2D1::ColorF(D2D1::ColorF::LightYellow)); D2D1_SIZE_F size = renderTarget->GetSize(); float rx = size.width / 2; float ry = size.height / 2; D2D1_ELLIPSE ellipse = D2D1::Ellipse(D2D1::Point2F(rx, ry), rx, ry); ComPtr<ID2D1SolidColorBrush> brush; AssertHR(renderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Orange), &brush)); renderTarget->FillEllipse(ellipse, brush.Get()); AssertHR(renderTarget->EndDraw()); }
WndProc
WM_SIZEにてRenderTargetのサイズを変更し、WM_PAINTにて上記のDraw関数を呼ぶよう実装します。
ComPtr<ID2D1HwndRenderTarget> g_renderTarget; LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_PAINT: Draw(g_renderTarget.Get()); return 0; case WM_SIZE: g_renderTarget->Resize(D2D1::SizeU(LOWORD(lParam), HIWORD(lParam))); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, msg, wParam, lParam); }
main
最終的にmain関数はこのようになります。
int main() { HWND hwnd = CreateHWND(WndProc); g_renderTarget = CreateRenderTarget(hwnd); Run(hwnd); return 0; }
glTFファイルの読み込み(WPF3Dで表示)
3Dデータ用のフォーマットは3dsやCOLLADA等たくさん存在しますが、2015年にKhronos GroupよりglTFというフォーマットが公開されました。
GitHub - KhronosGroup/glTF: glTF – Runtime 3D Asset Delivery
JSONベースで記述されていますが、頂点データなどは別ファイルにバイナリとして保存されているため、高速に読み込むことが可能です。
今回はglTFを読み込んで表示してみたいと思います。
読み込むデータは下記リンクにあるDuckを利用して実装していきたいと思います。
https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/Duck/glTF
glTFパース
現状すでにたくさんのパーサが存在していますが、今回はJSONライブラリを利用して自前でパースしてみたいと思います。
JSONパーサは下記のNewtonsoft.Jsonを利用しました。
NuGet Gallery | Json.NET 10.0.3
今回は必要最低限のデータをパースするよう、下記の様にGLTFクラスを準備しました。
public enum ComponentType { Int8 = 5120, UInt8 = 5121, Int16 = 5122, UInt16 = 5123, Float = 5126 } public enum DataType { VEC3, VEC2, SCALAR } public class Attributes { public int Normal { get; set; } public int Position { get; set; } public int Texcoord_0 { get; set; } } public class Primitive { public Attributes Attributes { get; set; } public int Indices { get; set; } } public class Mesh { public Primitive[] Primitives { get; set; } } public class Accessor { public int BufferView { get; set; } public int ByteOffset { get; set; } public ComponentType ComponentType { get; set; } public int Count { get; set; } public DataType Type { get; set; } } public class Image { public string Uri { get; set; } } public class BufferView { public int Buffer { get; set; } public int ByteOffset { get; set; } public int ByteLength { get; set; } } public class Buffer { public string Uri { get; set; } } public class GLTF { public Mesh[] Meshes { get; set; } public Accessor[] Accessors { get; set; } public Image[] Images { get; set; } public BufferView[] BufferViews { get; set; } public Buffer[] Buffers { get; set; } }
gltfをテキストとして読み込み、GLTFとしてDeserializeObjectを呼ぶことで、各々のプロパティにデータがセットされます。
Jsonテキストとデシリアライズ先のクラスにおいて、階層とプロパティ名が同じ(デフォルトでは大文字小文字関係なし)場合、そこにデータがデシリアライズされるようになっており、Jsonテキスト側かクラス側のどちらかにしか存在しないプロパティは無視されるようになっています。
そのため上記の様にクラスを定義することで、読み込みたいデータのみをパース出来るようになっています。
string str = File.ReadAllText(path);
GLTF gltf = JsonConvert.DeserializeObject<GLTF>(str);
詳しいフォーマットはGitHubのglTFサイトを参照してください。
パースしたglTFからメッシュデータに変換
glTFでは最終的にAccessorで定義された条件でバッファを参照することとなります。
GLTFクラスからバッファデータを取得する拡張メソッドを下記の様に定義しました。
バイナリデータは別のファイルに格納されているため、ファイルの存在するディレクトリパスを渡しています。(通常はgltfファイルと同じ場所にあると思います。)
public static int Size(this ComponentType type) => type == ComponentType.Float ? 4 : type == ComponentType.Int16 ? 2 : type == ComponentType.UInt16 ? 2 : 1; public static int Size(this DataType type) => type == DataType.SCALAR ? 1 : type == DataType.VEC2 ? 2 : 3; public static byte[][] AccessorBytes(this GLTF gltf, string directory) { var buffers = gltf.Buffers .Select(x => File.ReadAllBytes($"{directory}\\{x.Uri}")) .ToArray(); var bufferViews = gltf.BufferViews .Select(x => buffers[x.Buffer] .Skip(x.ByteOffset) .Take(x.ByteLength)) .ToArray(); var accessors = gltf.Accessors .Select(x => bufferViews[x.BufferView] .Skip(x.ByteOffset) .Take(x.Count * x.ComponentType.Size() * x.Type.Size()) .ToArray()) .ToArray(); return accessors; }
さらにバッファデータから実際にDuckメッシュを作成する、Duckクラスを定義します。
Primitive.IndicesやPrimitive.Attributes.Positionなどにバッファのインデクスが格納されているので、そのバッファからPoint3D[]などを読み込みます。(WPF3Dで表示するため)
本来、テクスチャはMaterial→Texture→Imageの階層で参照するのですが、簡単のためImageから直接読み込んでいます。
class Duck { public Point3D[] Positions { get; } public Vector3D[] Normals { get; } public Point[] Texcoords { get; } public int[] Indices { get; } public ImageSource Texture { get; } public Duck(string directory, GLTF gltf) { var accessorBytes = gltf.AccessorBytes(directory); var primitive = gltf.Meshes[0].Primitives[0]; Indices = ConvertTo(accessorBytes[primitive.Indices], 2, (bytes, start) => (int)BitConverter.ToUInt16(bytes, start) ); Positions = ConvertTo(accessorBytes[primitive.Attributes.Position], 12, (bytes, start) => new Point3D(BitConverter.ToSingle(bytes, start), BitConverter.ToSingle(bytes, start + 4), BitConverter.ToSingle(bytes, start + 8)) ); Normals = ConvertTo(accessorBytes[primitive.Attributes.Normal], 12, (bytes, start) => new Vector3D(BitConverter.ToSingle(bytes, start), BitConverter.ToSingle(bytes, start + 4), BitConverter.ToSingle(bytes, start + 8)) ); Texcoords = ConvertTo(accessorBytes[primitive.Attributes.Texcoord_0], 8, (bytes, start) => new Point(BitConverter.ToSingle(bytes, start), BitConverter.ToSingle(bytes, start + 4)) ); Texture = gltf.Images.Select(x => new BitmapImage(new Uri($"{directory}\\{x.Uri}"))).Single(); } T[] ConvertTo<T>(byte[] bytes, int stepBytes, Func<byte[], int, T> convert) { T[] dst = new T[bytes.Length / stepBytes]; for (int i = 0; i < dst.Length; ++i) dst[i] = convert(bytes, stepBytes * i); return dst; } }
WPF3Dで表示
MeshGeometry3Dにデータをセットし、Viewport3Dを作成します。
詳しくは下記記事を参照してください。
WPFのクラスで3Dプログラミング - 何でもプログラミング
var model = new GeometryModel3D() { Geometry = new MeshGeometry3D() { Positions = new Point3DCollection(mesh.Positions), Normals = new Vector3DCollection(mesh.Normals), TextureCoordinates = new PointCollection(mesh.Texcoords), TriangleIndices = new Int32Collection(mesh.Indices) }, Material = new DiffuseMaterial(new ImageBrush(mesh.Texture)), }; var light = new DirectionalLight(Colors.White, new Vector3D(0, 0, -1)); var camera = new PerspectiveCamera(new Point3D(0, 100, 300), new Vector3D(0, 0, -1), new Vector3D(0, 1, 0), 45); Viewport3D viewport3D = new Viewport3D(); viewport3D.Camera = camera; viewport3D.Children.Add(new ModelVisual3D() { Content = model }); viewport3D.Children.Add(new ModelVisual3D() { Content = light }); Content = viewport3D;
WPFのクラスで3Dプログラミング
WPFで3Dプログラミングをする場合、通常は裏でDirect3Dを用意してレンダリングを行います。
ただ、とても簡単な3D描画を行いたい場合は、WPFが用意する3D描画クラスを利用することができます。
今回は簡単な実装をしてみたいと思います。
アプリケーション
今回はキーボードの上下左右で回転する立方体を表示してみたいと思います。
コントロールの主体はViewport3Dで、そこにカメラやライト、メッシュを追加していく形となります。
法線は自動生成されるため、今回は作成していません。
回転はModel3DのTransformを更新することで行っています。
下記はC#で実装していますが、Xamlで記述することも可能です。
// Cubeの各三角形(12枚)を作成 var vertices = new [] { new Point3D(-1, 1, 1), new Point3D(-1, -1, 1), new Point3D( 1, -1, 1), new Point3D( 1, 1, 1), new Point3D(-1, 1, -1), new Point3D(-1, -1, -1), new Point3D( 1, -1, -1), new Point3D( 1, 1, -1) }; Point3D[] face(int i1, int i2, int i3, int i4) => new[] { i1, i2, i3, i1, i3, i4 }.Select(x => vertices[x]).ToArray(); Point3D[] positions = new[] { face(0, 1, 2, 3), face(3, 2, 6, 7), face(7, 6, 5, 4), face(4, 5, 1, 0), face(0, 3, 7, 4), face(5, 6, 2, 1), }.SelectMany(x => x).ToArray(); // 頂点座標と色をセット var model = new GeometryModel3D() { Geometry = new MeshGeometry3D() { Positions = new Point3DCollection(positions) }, Material = new DiffuseMaterial(Brushes.LightGreen), }; // ライト作成 var light = new DirectionalLight(Colors.White, new Vector3D(0, 0, -1)); // カメラ作成 var camera = new PerspectiveCamera(new Point3D(0, 0, 5), new Vector3D(0, 0, -1), new Vector3D(0, 1, 0), 45); // 表示用コントロールの作成 Viewport3D viewport3D = new Viewport3D(); viewport3D.Camera = camera; viewport3D.Children.Add(new ModelVisual3D() { Content = model }); viewport3D.Children.Add(new ModelVisual3D() { Content = light }); Content = viewport3D; // KeyDownでCubeが回転するよう実装 var quaternion = new Quaternion(); KeyDown += (s, e) => { var q = e.Key == Key.Left ? new Quaternion(new Vector3D(0, 1, 0), -1) : e.Key == Key.Right ? new Quaternion(new Vector3D(0, 1, 0), 1) : e.Key == Key.Up ? new Quaternion(new Vector3D(1, 0, 0), -1) : e.Key == Key.Down ? new Quaternion(new Vector3D(1, 0, 0), 1) : Quaternion.Identity; quaternion = q * quaternion; model.Transform = new RotateTransform3D(new QuaternionRotation3D(quaternion)); };
法線やインデックス
MeshGeometry3DにはNormalsとTriangleIndicesが設定可能となっています。
テクスチャ
MeshGeometry3DのTextureCoordinatesを設定し、DiffuseMaterialにImageBrushを設定します。
カリングOFF
GeometryModel3DのBackMaterialを設定するとOFFになります。
ライン描画
残念ながら対応していません。細いポリゴンを描くしかなさそうです。
オブジェクト毎のマウスイベントハンドリング
ModelVisual3Dの代わりにModelUIElement3Dを利用すると、MouseMoveなどが登録できるようになります。
2Dコントロール(Buttonなど)を3D上に配置
Viewport2DVisual3Dを用いると可能となります。
JavaScript(ES6)でElm Architecture(Virtual-DOMなし) --- 可変個コントロール
下記記事にてJavaScriptにElmのModelとUpdateの機構を取り入れてみました。
JavaScript(ES6)でElm Architecture(Virtual-DOMなし) - 何でもプログラミング
今回は引き続き、可変個のコントロールに対応してみたいと思います。
アプリケーションコード
カウンターを追加、削除できるアプリケーションを実装してみたいと思います。
可変個のコントロールの定義として、htmlのtemplate要素を利用しています。
また可変個コントロールの動作の実装にbindItemsを利用しています。(実装はのちほど)
その他CellクラスやstartApp関数は下記記事を参照してください。
JavaScript(ES6)でElm Architecture(Virtual-DOMなし) - 何でもプログラミング
<button id="add">add</button> <button id="remove">remove</button> <!-- 可変個カウンタ --> <div id="counts"> <template> <div> <button class="dec">dec</button> <input type="text" class="count"></input> <button class="inc">inc</button> </div> </template> </div>
const initialModel = { counts : [] }; const message = { addCounter : Symbol(), removeCounter : Symbol(), increment : Symbol(), decrement : Symbol() } function update(model, msg){ switch (msg.id) { case message.addCounter : return copy(model, {counts : model.counts.insertLast(0)}); case message.removeCounter : return copy(model, {counts : model.counts.removeLast()}); case message.increment : return copy(model, {counts : model.counts.updateAt(msg.index, x => x + 1)}); case message.decrement : return copy(model, {counts : model.counts.updateAt(msg.index, x => x - 1)}); default : throw "invalid message" } } function bind(model, send) { byId("add").onclick = () => send({ id: message.addCounter }); byId("remove").onclick = () => send({ id: message.removeCounter }); // 可変個カウンタBinding bindItems(model.map(x => x.counts), "counts", (item, index, element) => { const byClass = x => element.getElementsByClassName(x)[0]; item.listen(x => byClass("count").value = x); byClass("dec").onclick = () => send({ id: message.decrement, index: index }); byClass("inc").onclick = () => send({ id: message.increment, index: index }); }); } window.onload = () => startApp(initialModel, update, bind);
bindItems
itemsのCell、親要素のID、コントロールのbindingを受け取る関数になります。
要素内のtemplateを取得し、実際にコントロールを配置するdiv要素を追加しています。
itemsが変更されたときに、変更の送出 or コントロールの作成 or コントロールの削除を行っています。
function bindItems(itemsCell, parentId, bind) { const parent = byId(parentId); const itemTemplate = parent.getElementsByTagName("template")[0]; const panel = document.createElement("div"); parent.appendChild(panel); const cells = []; itemsCell.listen(items => { for (let i = 0; i < items.length; i++) { // コントロールが存在 if (i < cells.length) { cells[i].send(items[i]); } // コントロール不足 else { const cell = new Cell(items[i]); const element = document.importNode(itemTemplate.content, true); cells.push(cell); panel.appendChild(element); bind(cell, i, panel.lastElementChild); } } // コントロール過剰 for (let i = 0; i < cells.length - items.length; i++) { cells.pop(); panel.lastElementChild.remove(); } }); }
外側のスコープのCellをbind内で利用するとリーク
スコープ外のCellを利用すると、コントロールの参照がCell内に残り続けてリークを起こしてしまいます。
そのため、スコープ外のCellを利用する場合は、一旦キャプチャしてbind関数の引数に追加する必要があります。
実装例ですが、C#のものが下記にありますので参照してください。
WPFでElm Architecture --- Xaml利用しない版 --- 可変個のコントロール - 何でもプログラミング
JavaScript(ES6)でElm Architecture(Virtual-DOMなし)
Elm自体がJavaScriptを生成するものなので実際には利用することはないと思いますが(さらに生のJavaScriptがよければReact.jsがあります)、ElmのModelとUpdateの仕組みを生のJavaScriptで実装してみました。
WPFとC#で実装されたものは、下記記事にて参照できます。
WPFでElm Architecture --- Xaml利用しない版 - 何でもプログラミング
アプリケーションコード
簡単なカウンタアプリを実装します。
initialModel | 状態の初期値を定義 |
message | update関数で処理させる内容の識別子 |
update | 現在のmodelと、messageを受け取って、新しいmodelを返す |
bind | modelのCell(現在値+変更通知)と、send(message)関数を、html要素にバインドする |
CellはFunctional Rective Programming(FRP)でのBehaviorを意図しており、詳しくは下記を参照してください。
SodiumでFunctional Reactive Programming (F#) --- 導入 - 何でもプログラミング
<body> <button id="dec">dec</button> <input type="text" id="count"></input> <button id="inc">inc</button> </body>
const initialModel = { count : 0, }; const message = { increment : Symbol(), decrement : Symbol(), } function copy(obj, diff) { return Object.assign({}, obj, diff); } function update(model, msg){ switch (msg.id) { case message.increment : return copy(model, {count : model.count + 1}); case message.decrement : return copy(model, {count : model.count - 1}); default : throw "invalid message" } } function byId(name) { return document.getElementById(name); } function bind(model, send) { model.map(x => x.count).listen(x => byId("count").value = x); byId("dec").onclick = () => send({ id: message.decrement }); byId("inc").onclick = () => send({ id: message.increment }); } window.onload = () => startApp(initialModel, update, bind);
Cellクラス
listenでコールバックを登録し、sendでデータを送出し、mapでデータの変換を行います。
listenした後、コールバックを解除する機能は実装してありませんので、利用する際(コントロールを動的に追加or削除)はリークに注意してください。
FRPのライブラリSodiumにJavaScript版がありますので、そちらのCellを利用することも可能です。
class Cell { constructor(initialValue) { this.value = initialValue; this.callbacks = []; } listen(f) { f(this.value); this.callbacks.push(f); } send(value) { if (this.value === value) return; this.value = value; this.callbacks.forEach(f => f(value)); } map(f) { const cell = new Cell(f(this.value)); this.callbacks.push(x => cell.send(f(x))); return cell; } }
startApp
initialModelとupdateから、modelのCellとsend関数を作成し、bindを呼び出しています。
function startApp(initialModel, update, bind){ const cell = new Cell(initialModel); const send = msg => cell.send(update(cell.value, msg)); bind(cell, send); }
WPFでElm Architecture --- Xaml利用しない版 --- 可変個のコントロール --- データによりコントロールの種類変更
下記記事にて可変個のコントロールを表示できるよう実装しました。
WPFでElm Architecture --- Xaml利用しない版 --- 可変個のコントロール - 何でもプログラミング
ただし、すべてのコントロールが同じで個数のみが可変であったため、今回は入力データによってコントロールが分岐するような実装をしてみたいと思います。
アプリケーションコード
ボタンを押すと対応した図形が追加され、図形上で左クリックでサイズが大きくなり、図形上で右クリックをすると削除されるようなアプリケーションを作成してみます。
itemsとselectorとfactoryが渡せるwith_Childrenを用いてコントロールの切り替えを実現しています。(with_Childrenの実装は後述)
コード内で利用されているLINQオペレータは下記記事を参照してください。
LINQ独自オペレータ メモ - 何でもプログラミング
図形モデル定義
interface IShape { double Size { get; } } class Circle : IShape { public double Size { get; } public Circle(double size) { Size = size; } } class Square : IShape { public double Size { get; } public Square(double size) { Size = size; } } class Triangle : IShape { public double Size { get; } public Triangle(double size) { Size = size; } }
アプリケーションモデル定義
class Model { public IShape[] Shapes { get; } public Model(IShape[] shapes) { Shapes = shapes; } }
モデル更新メッセージ定義
abstract class Message { } class AddCircle : Message { } class AddSquare : Message { } class AddTriangle : Message { } class IncrementSize : Message { public int Index { get; } public IncrementSize(int index) { Index = index; } } class RemoveShape : Message { public int Index { get; } public RemoveShape(int index) { Index = index; } }
モデル更新関数
public static Model Update(Model model, Message message) { switch (message) { case AddCircle msg: return new Model(model.Shapes.InsertLast(new Circle(30)).ToArray()); case AddSquare msg: return new Model(model.Shapes.InsertLast(new Square(30)).ToArray()); case AddTriangle msg: return new Model(model.Shapes.InsertLast(new Triangle(30)).ToArray()); case IncrementSize msg: { IShape increment(IShape src) => src is Circle ? new Circle(src.Size + 10) as IShape : src is Square ? new Square(src.Size + 10) as IShape : new Triangle(src.Size + 10) as IShape; return new Model(model.Shapes.UpdateAt(msg.Index, increment).ToArray()); } case RemoveShape msg: return new Model(model.Shapes.RemoveAt(msg.Index).ToArray()); default: throw new Exception("invalid message"); } }
View作成関数
enum Shape { Circle, Square, Triangle } public static FrameworkElement Create(ICell<Model> model, Action<Message> send) { return new StackPanel() .with_Margin(10, 10, 10, 10) .with_Children( new Button() .with_Content("Circle追加") .with_Margin(5, 5, 5, 5) .with_Click(() => send(new AddCircle())), new Button() .with_Content("Square追加") .with_Margin(5, 5, 5, 5) .with_Click(() => send(new AddSquare())), new Button() .with_Content("Triangle追加") .with_Margin(5, 5, 5, 5) .with_Click(() => send(new AddTriangle())), new StackPanel() .with_Children( model.Map(x => x.Shapes), item => item is Circle ? Shape.Circle : item is Square ? Shape.Square : Shape.Triangle, (id, item, index) => { var element = id == Shape.Circle ? new Ellipse().with_Fill(Brushes.Red) as FrameworkElement: id == Shape.Square ? new Rectangle().with_Fill(Brushes.Green) as FrameworkElement: new Path().with_Data(Geometry.Parse("M0,100 H100 L50,0 Z")).with_Stretch(Stretch.Fill).with_Fill(Brushes.Blue) as FrameworkElement; return element .with_Margin(5, 5, 5, 5) .with_Width(item.Map(x => x.Size)) .with_Height(item.Map(x => x.Size)) .with_MouseLeftButtonDown(x => send(new IncrementSize(index))) .with_MouseRightButtonDown(x => send(new RemoveShape(index))); } ) ); }
with_Children
固定コントロールの場合のwith_Childrenと比べて、入力データからコントロールの識別子を返すselector関数が追加され、factoryの引数にも識別子が渡されるようになっています。
新しいデータのコントロール識別子に変更がなければそのままデータを送出し、識別子が異なる場合はコントロールを作り直しています。
もっと賢い差分アルゴリズムを利用すると、コントロールの生成が最小限に抑えられると思います。
public static TPanel with_Children<TPanel, TItem, TViewID>( this TPanel element, ICell<TItem[]> items, Func<TItem, TViewID> selector, Func<TViewID, ICell<TItem>, int, FrameworkElement> factory) where TPanel : Panel { List<CellSink<TItem>> sinks = new List<CellSink<TItem>>(); List<TViewID> viewIDs = new List<TViewID>(); element.Children.Clear(); items.Listen(xs => { for (int i = 0; i < xs.Length; ++i) { // コントロールが存在 if (i < sinks.Count) { TViewID viewID = selector(xs[i]); // ViewID変更なし if (Equals(viewID, viewIDs[i])) { sinks[i].Send(xs[i]); } // ViewIDが変わった else { sinks[i] = new CellSink<TItem>(xs[i]); viewIDs[i] = viewID; element.Children.RemoveAt(i); element.Children.Insert(i, factory(viewID, sinks[i], i)); } } // コントロールが足りない else { sinks.Add(new CellSink<TItem>(xs[i])); viewIDs.Add(selector(xs[i])); element.Children.Add(factory(viewIDs.Last(), sinks.Last(), i)); } } // コントロールが過剰 for (int i = 0; i < sinks.Count - xs.Length; ++i) { sinks.RemoveAt(sinks.Count - 1); viewIDs.RemoveAt(viewIDs.Count - 1); element.Children.RemoveAt(element.Children.Count - 1); } }); return element; }
factoryの外側のスコープのCellを利用する場合
そのままCellを利用するとリークを起こしてしまうので、別途追加のCellを受け取るwith_Childrenが必要となります。
下記記事にても同様の実装がありますので、こちらを参照してください。
WPFでElm Architecture --- Xaml利用しない版 --- 可変個のコントロール - 何でもプログラミング
Throttle(一定時間リクエストがなければ実行をする)の実装
スライダーなどから大量のリクエストが送られてきたときに、全て処理する必要がない場合、一定時間リクエストがなければ最新の値で処理を行うことがあります。
ReactiveExtensionにもThrottleオペレータは用意されていますので、普段はこれを利用するので問題ないと思います。
GitHub - Reactive-Extensions/Rx.NET: The Reactive Extensions for .NET
今回はThrottle処理を行うクラスを実装してみたいと思います。
Throttleクラス
Delayで処理を遅らせたTaskを利用します。
リクエストが来たら、現在のTaskをキャンセルして新しいTaskを走らせる、とても簡単な内容になっています。
public class Throttle { CancellationTokenSource _cancel = new CancellationTokenSource(); int _milliseconds; public Throttle(int milliseconds) => _milliseconds = milliseconds; public void Post(Action action) { // 現在のTaskをキャンセル _cancel.Cancel(); _cancel = new CancellationTokenSource(); // 指定時間待ったあと処理を行う Task.Delay(_milliseconds, _cancel.Token) .ContinueWith(task => action(), _cancel.Token); } }
動作確認
スライダーを動かすと、値がテキストに反映されるアプリを作成してみます。
スライダーを動かしてから1秒以内にまたスライダーを動かす限り、テキストが更新されません。
現状Postは別スレッドで実行されるため、InvokeAsyncでUIスレッドでテキストを更新しています。
var throttle = new Throttle(1000); slider.ValueChanged += (s, e) => throttle.Post(() => textblock.Dispatcher.InvokeAsync(() => textblock.Text = ((int)e.NewValue).ToString()));