ネイティブからXamlでGUI作成する方法を探した。実装がよくわかってないので内部的には.NETかUWPに依存しているのかもしれない。未確認。少なくともC++17から呼び出すことが可能。
MS公式
C++ デスクトップ (Win32) アプリで標準 WinRT XAML コントロールをホストする
WinRTのプロジェクトを作成。ここでUWPが含まれるものを選択しないように注意
上記からプロジェクトを作るとデフォルトでマニフェストファイルが生成されているので、マニフェストツールから追加のマニフェストファイルに登録する。
マニフェストファイルの内容を以下のように設定。バージョンはWindows10以降である必要がある。
<?xml version="1.0" encoding="UTF-8"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> <application> <!-- Windows 10 --> <maxversiontested Id="10.0.18362.0"/> <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /> </application> </compatibility> </assembly>
続いて、参照を右クリック→参照の追加 を選択。
ファイルを追加。
以下のようにコードを記述。
#include "pch.h" #include <windows.h> #include <stdlib.h> #include <string.h> #include <winrt/Windows.Foundation.Collections.h> #include <winrt/Windows.system.h> #include <winrt/windows.ui.xaml.hosting.h> #include <windows.ui.xaml.hosting.desktopwindowxamlsource.h> #include <winrt/windows.ui.xaml.controls.h> #include <winrt/Windows.ui.xaml.media.h> using namespace winrt; using namespace Windows::UI; using namespace Windows::UI::Composition; using namespace Windows::UI::Xaml::Hosting; using namespace Windows::Foundation::Numerics; // Microsoftのサンプルをそのままビルドすると // TextOutでリンクエラーが出るのでこれらを追加しておく #pragma comment(lib,"gdi32.lib") LRESULT CALLBACK WindowProc(HWND, UINT, WPARAM, LPARAM); HWND _hWnd; HWND _childhWnd; HINSTANCE _hInstance;
int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow) { _hInstance = hInstance; // The main window class name. const wchar_t szWindowClass[] = L"Win32DesktopApp"; WNDCLASSEX windowClass = { }; windowClass.cbSize = sizeof(WNDCLASSEX); windowClass.lpfnWndProc = WindowProc; windowClass.hInstance = hInstance; windowClass.lpszClassName = szWindowClass; windowClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); windowClass.hIconSm = LoadIcon(windowClass.hInstance, IDI_APPLICATION); if (RegisterClassEx(&windowClass) == NULL) { MessageBox(NULL, L"Windows registration failed!", L"Error", NULL); return 0; } _hWnd = CreateWindow( szWindowClass, L"Windows c++ Win32 Desktop App", WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL ); if (_hWnd == NULL) { MessageBox(NULL, L"Call to CreateWindow failed!", L"Error", NULL); return 0; } // Begin XAML Island section. // The call to winrt::init_apartment initializes COM; by default, in a multithreaded apartment. winrt::init_apartment(apartment_type::single_threaded); // Initialize the XAML framework's core window for the current thread. WindowsXamlManager winxamlmanager = WindowsXamlManager::InitializeForCurrentThread(); // This DesktopWindowXamlSource is the object that enables a non-UWP desktop application // to host WinRT XAML controls in any UI element that is associated with a window handle (HWND). DesktopWindowXamlSource desktopSource; // Get handle to the core window. auto interop = desktopSource.as<IDesktopWindowXamlSourceNative>(); // Parent the DesktopWindowXamlSource object to the current window. check_hresult(interop->AttachToWindow(_hWnd)); // This HWND will be the window handler for the XAML Island: A child window that contains XAML. HWND hWndXamlIsland = nullptr; // Get the new child window's HWND. interop->get_WindowHandle(&hWndXamlIsland); // Update the XAML Island window size because initially it is 0,0. SetWindowPos(hWndXamlIsland, 0, 200, 100, 800, 200, SWP_SHOWWINDOW); // Create the XAML content. Windows::UI::Xaml::Controls::StackPanel xamlContainer; xamlContainer.Background(Windows::UI::Xaml::Media::SolidColorBrush{ Windows::UI::Colors::LightGray() }); Windows::UI::Xaml::Controls::TextBlock tb; tb.Text(L"Hello World from Xaml Islands!"); tb.VerticalAlignment(Windows::UI::Xaml::VerticalAlignment::Center); tb.HorizontalAlignment(Windows::UI::Xaml::HorizontalAlignment::Center); tb.FontSize(48); xamlContainer.Children().Append(tb); xamlContainer.UpdateLayout(); desktopSource.Content(xamlContainer); // End XAML Island section. ShowWindow(_hWnd, nCmdShow); UpdateWindow(_hWnd); //Message loop: MSG msg = { }; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return 0; }
/////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////// LRESULT CALLBACK WindowProc(HWND hWnd, UINT messageCode, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; HDC hdc; wchar_t greeting[] = L"Hello World in Win32!"; RECT rcClient; switch (messageCode) { case WM_PAINT: if (hWnd == _hWnd) { hdc = BeginPaint(hWnd, &ps); TextOut(hdc, 300, 5, greeting, wcslen(greeting)); EndPaint(hWnd, &ps); } break; case WM_DESTROY: PostQuitMessage(0); break; // Create main window case WM_CREATE: _childhWnd = CreateWindowEx(0, L"ChildWClass", NULL, WS_CHILD | WS_BORDER, 0, 0, 0, 0, hWnd, NULL, _hInstance, NULL); return 0; // Main window changed size case WM_SIZE: // Get the dimensions of the main window's client // area, and enumerate the child windows. Pass the // dimensions to the child windows during enumeration. GetClientRect(hWnd, &rcClient); MoveWindow(_childhWnd, 200, 200, 400, 500, TRUE); ShowWindow(_childhWnd, SW_SHOW); return 0; // Process other messages. default: return DefWindowProc(hWnd, messageCode, wParam, lParam); break; } return 0; }
実行例
続く。
Rustのライフタイムについて。
まず、多くのプログラミング言語では、「スコープ」という概念がある。
これは変数の寿命のことで、変数は例えば{}で括られた範囲でしか有効ではない。
この仕組みはRustにもある。
#include <iostream> int main() { { int value = 10; value = 20; } printf("%d\n", value ); // コンパイルエラー valueはこのスコープに存在しない }
fn main( ){ { let mut value = 10; value = 20; } println!("{}",value);// コンパイルエラー valueはこのスコープに存在しない }
それとは別に、Rustには「ライフタイム」という概念がある。
ライフタイムは「参照の有効範囲」のことで、C++などでは参照が実は切れていることを、実行時にしか判別できない(下コード左)。
しかしRustではコンパイルの段階で明らかに参照が切れているような状態を検出できる(下コード右)ので、参照が切れるようなコードはそもそもコンパイルが通らず、実行すること自体ができない。
故にRustでは「ビルドが通ったなら安易な参照切れは起こしていない」と言える。
#include <iostream> struct AB { int a, b; }; int main() { auto myref = std::make_unique<AB>(AB{ 5,10 }); { auto ab = std::move(myref); // ここで myrefからabへmove printf("%d\n", ab->a); } // 実行時エラー 参照先が無効 auto k = myref->a; printf("%d\n", k); }
struct AB{ a:i32, b:i32, } ////////////////////////////////////////////////// fn main( ){ let myref=AB{a:5,b:10}; { let ab=myref; // ここで myrefからabへmove println!("{}",ab.a); } // コンパイルエラー 参照先が無効 let k = myref.a; println!("{}",k); }
まず以下のように定義する。
struct IntValue ... TraitMyDisp と TraitMyPow 実装
struct FloatValue ... TraitMyPow のみ実装
// これをimplementしているときは表示できる trait TraitMyDisp{ fn disp(&self); } // これをimplementしているときは値を二乗する trait TraitMyPow{ fn get_pow2(&self)->f32; } ////////////////////////////////////////// //////////////////////////////////////////struct IntValue{ value: i32 } impl TraitMyDisp for IntValue{ fn disp(&self){ println!("{}",self.value); } } impl TraitMyPow for IntValue{ fn get_pow2(&self)->f32{ ( self.value*self.value ) as f32 } }////////////////////////////////////////// //////////////////////////////////////////struct FloatValue{ value:f32 } impl TraitMyPow for FloatValue{ fn get_pow2(&self)->f32{ ( self.value*self.value ) as f32 } }
まず、以下のようにただ<T>を渡してもコンパイルは通らない。
// C++だと T に get_pos2があればビルドが通るが // Rustでは valueに対して何をできるのかを指定しなければならない fn call<T>(value:T){ println!("call {}",value.get_pow2()); // NG T にget_pow2が必要なんて聞いてない }
fn main(){ // TraitMyDisp , TraitMyPow の両方が実装済み let i_val = IntValue{value:2}; // TraitMyPow のみが実装済み let f_val = FloatValue{value:2.0}; call(i_val); }
「型Tを取る」ではなく、「get_pow2()を持つ型Tを取る」と記述しなければいけない。
// T は TraitMyPow を実装していなければならない fn call< T : TraitMyPow >(value:T){ println!("call {}",value.get_pow2()); }
トレイトは + で複数指定できる。
T は+で指定されたすべてのトレイトに対応していなければならない。
たとえその関数の中で使っていなくても、実装だけはしていなければならない。
// <T:トレイト + トレイト> のように+で複数指定できる fn call_both<T:TraitMyPow + TraitMyDisp>(value:&T){ // TraitMyDisp::dispは使っていないが、トレイト境界を指定しているので // Tがdispに対応していないならコンパイルできない(たとえ使っていなくても) println!("call {}",value.get_pow2()); } fn main(){ // TraitMyDisp , TraitMyPow の両方が実装済み let i_val = IntValue{value:2}; // TraitMyPow のみが実装済み let f_val = FloatValue{value:2.0}; call_both(&i_val); // OK // call_both(&f_val); // NG FloatValueのためのTraitMyDispはimplしていない }
Tに渡される型が + 演算できるかどうかはわからない。
したがって、例え自分がプリミティブな数値型しか与えるつもりがなくても、コンパイラはそんなことは知らないので「TはAddが可能である」ことを教えなければいけない。
// Addモジュールを使う use std::ops::Add; ///////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////
// add関数の定義 // Tの必須の振る舞いとしてAddを指定。これで operator + が使える fn add< T : Add<Output=T> >( v1:T , v2:T ) -> T { v1 + v2 }
fn main(){ let val:i32 = add( 3,4); }
Rustには継承はないがポリモーフィズムはある。C++やJavaのようなオブジェクト指向ではポリモーフィズムの実現のために継承があるようなイメージがあるので、この関係が奇妙に見える。
継承とは「鷹も燕も"鳥"の特性をすべて持ち、その他に若干の差異がある同種のモノである」という考え方をする。だからどうしても子クラスが肥大化していく。そのあたりを嫌った結果らしい。
// 振る舞い trait Flying{ fn fly(&self); }
// 構造体 鷹 struct Hawk{ weight:f32, speed:f32, } // トレイトのR実装 impl Flying for Hawk{ // メソッド fn fly(&self){ println!("鷹が飛ぶ {} km/h \n",self.speed ); } }
// 構造体 ハチドリ struct Hummingbird{ weight:f32, speed:f32, } // トレイトのR実装 impl Flying for Hummingbird{ // メソッド fn fly(&self){ println!("ハチドリが飛ぶ {} km/h\n", self.speed); } }
fn main() { // スタック上に確保する固定長配列は [T;N]の形をしている // T ... Type , N ... 個数 let ary:[ Box<dyn Flying> ; 2 ] = [ // Boxはヒープにメモリを確保する。 Box::new(Hawk{weight:1100.0,speed:190.0}), // アカオノスリ Box::new(Hummingbird{weight:20.0,speed:98.0}) // ハチドリ ]; ary[0].fly(); ary[1].fly(); }
C++の継承が「Birdの特性を持った鷹を実装」という形であるのに対し、
Rustのtraitは「Flyという振る舞いを、鷹や燕の特性に付与する」というような発想をすると、現時点ではとらえている(下図、現時点の解釈)
C++のオブジェクト指向だと飛べないはずのペンギンもflyを持たざるを得ないが、Rustのtraitであればペンギンにはflyを付与しなければよいだけの話だということになる。
配列にはいくつか種類があるらしい。
・[T;N] ... スタック上に固定長配列
・Vec<T> ヒープ上の可変長配列
・Box<[T;N]>ヒープ上に固定長配列
注意点として、基本的に配列の要素は全て初期化しなければならない。可変長配列でcapacityを指定できるVecはともかく、それ以外をメモリ確保だけして初期化したくない場合には、std::mem::MaybeUninitを使わなければならない。これはunsafeで囲まなければならない。
fn main() { //////////////////////////////////////////// // 固定長配列(スタック)
let mut ary1:[i32;5]=[1,2,3,4,5]; for a1 in ary1{ print!("{} ",a1); } println!("");
// 初期化しないバージョン let mut ary2:[i32;5] = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; for a2 in ary2{ println!("* {} ",a2); } println!("\n");
//////////////////////////////////////////// // 可変長配列
let mut vec1:Vec<&str> = vec!["a", "b", "c"]; vec1.push("d"); for v1 in vec1{ print!("{} ",v1); } println!("");
// 初期化しないバージョン let mut vec2:Vec<&str> = vec![]; vec2.push("d"); for v2 in vec2{ print!("* {} ",v2); } println!("\n");
//////////////////////////////////////////// // 固定長配列(ヒープ)
let mut box1:Box<[f32;5]> = Box::new([10.5 , 11.5 , 12.5 , 13.5 , 14.5]); for b1 in box1.iter(){ print!("{} ",b1); } println!("");
// 初期化しないバージョン let mut box2:Box<[f32;5]> = Box::new(unsafe { std::mem::MaybeUninit::uninit().assume_init() } ); for b2 in box2.iter(){ println!("* {} ",b2); }
}
初期化しなければ不正な値が入っていることを確認できた。
1 2 3 4 5
* 112
* 0
* -580595040
* 367
* -580614544
a b c d
* d
10.5 11.5 12.5 13.5 14.5
* 2
* 0
* -1030514000000000000
* 0.000000000000000000000000000000000000000000514
* 0.000000000000000000000000000000000000000000006
構造体関連で、struct,impl,trait の役割は以下(だと考えられる)
本当はポリモーフィズムをやりたかったがどうしても理解が追い付かなかったので省略。
// #[derive(Debug)] をつけるとprintln!でそのまま表示できるようになる #[derive(Debug)] // 構造体の定義。 struct Rectangle{ width:i32, height:i32, }
fn main() { // 初期化なしでは定義できない //let mut rr = Rectangle{}; let mut rr = Rectangle{width:0,height:0}; // mut をつけているので代入できる rr.width = 20; rr.height = 20; // #[derive(Debug)] をつけているので {:?} で表示できる println!("{:?}",rr); }
// 構造体の定義 struct Rectangle{ width:i32, height:i32, }
// implの中でメソッドや関連関数を定義 impl Rectangle{ // Rectangleのメソッドを定義 メソッドはselfをとる。 selfがないなら関連関数 fn area(&self)->i32{ return self.width * self.height; } }
fn main() { // インスタンス生成 let mut rr = Rectangle{width:0,height:0}; rr.width = 20; rr.height = 20; // メソッド呼び出し println!("{}",rr.area() ); }
implの中に&selfをとらない関数を定義すると関連関数になる。
新規インスタンスを作成する関連関数を定義する場合、名前をnewにするのが一般的らしい。
呼び出しは 構造体名::関連関数名() となる。
// 構造体の定義 struct Rectangle{ width:i32, height:i32, } impl Rectangle{ // 関連関数。 &selfをとらない。 // Rectangle::new で呼び出す。 // 新規インスタンス生成関連関数は「new」にする習慣がある fn new()->Rectangle{ return Rectangle{width:20,height:30}; } // メソッド fn area(&self)->i32{return self.width * self.height;} }
fn main() { // 関連関数でインスタンス生成 let mut rr = Rectangle::new(); println!("{}",rr.area() ); }
implをbehavior_calc_areaの実装とすることで、areaメソッドの実装を強制することができる。
// 振舞いの定義 trait behavior_calc_area{ fn area(&self)->f32; }
// 構造体の定義 struct Rectangle{ width:i32, height:i32, } // トレイトのR実装 impl behavior_calc_area for Rectangle{ // メソッド fn area(&self)->f32{ return (self.width * self.height) as f32; } }
struct Triangle{ width:i32, height:i32, } // トレイトのR実装 impl behavior_calc_area for Triangle{ // メソッド fn area(&self)->f32{ return (self.width * self.height) as f32 / 2.0 ; } }
fn main() { // インスタンス生成 let t:Triangle = Triangle{width:10,height:5}; let r:Rectangle = Rectangle{width:10,height:5}; println!("{}\n",t.area() ); println!("{}\n",r.area() ); }
基本はそんなに複雑ではないのだが、呼び出すときに暗黙型変換してくれないらしく、明示的型変換(as f32) を入れないといけないらしい。
// 関数定義 // 値渡し fn add(a:f32,b:f32)->f32{ return a+b; } fn main() { // 関数呼び出し // 引数に整数を入れてもaddのf32に暗黙変換できないらしいので // as f32 で実数に型変換する let k = add(3 as f32,5 as f32); println!("k= {}",k); }
不変参照で渡すときは渡す側と受ける側で&をつける。
// デフォルトで不変参照。C++でいうところのconst。kの内容を変更できない fn add2(k: &i32)->f64{ // &mutでないときは *k としなくていいらしい return (k + 2) as f64; }
fn main() { let a:i32=3; // mutがついていないので変更不可 println!(" {}", add2(&a) ); // aの定義時にmutをつけていないのでここもただの& }
可変参照は型名の前に&mut をつける。
呼び出す側でも&mutが必要。
さらにこの値を使うときは参照外しとして*をつけなければならない。もっとほかの記号はなかったのか。
// &mut で可変参照 fn swap(i:&mut i32,j:&mut i32){ let k:i32; // &mutの時は頭に*をつけて *i とする k = *i; *i = *j; *j = k; }
fn main() { let mut a:i32=3; let mut b:i32=8; swap(&mut a,&mut b); println!("{} {}",a,b); }
C++のテンプレートのようなgenericsという機能があるが、ただ<T>としただけでは動かない。printlnすらできない。いずれ詳しく調べる。
https://www.koderhq.com/tutorial/rust/generics/
use std::ops::Mul; fn square<T: Mul<Output = T> + Copy >(val:T)->T{ return val*val; }
use std::fmt::Display; fn console_log<T: Display> (x: T) { println!("{}", x); }
fn main() { println!("{}", square(3) ); console_log( 3 ); }
fn main() { for x in 1..=9{ for y in 1..=9{ print!("{} ",x*y); } println!(""); } }
なんとRustのforにはCのforのようにカウンタを使う記述法がない。その手のものが必要な時はwhileを使う。さらにRustにはインクリメント演算子++がない。+=はあるのでそれを使う。
なぜ++がないのかよくわからないが、コンパイラが進化した今inc命令とadd命令をプログラマが区別する必要はないのでそのために言語仕様を複雑にする必要はないとかそんな感じではないだろうか(適当)。
fn main() { let mut k = 0;// 変数はデフォルトで変更不可なのでmutをつける while k < 10{ println!("k=={}",k); k+=1; // Rustには++がない } }
ifはCとかなり近い。ただし()で囲う必要がない。
fn main() { let k = 3; if k == 1{ println!("k==1"); } else if k==2{ println!("k==2"); } else{ println!("other k"); } }
特に違うものとして、Cの三項演算子に該当するものがifで表記するようになっている。
fn main() { let k = 2; let msg = if k%2!=0 {"odd"}else{"even"}; println!("{}",msg); }
いろいろとCのswitchに近いがbreakしないところが特に違う。あとカンマ区切り。
fn main() { let i = 2; // switchに近い。 // ただし、matchの中で、「取りうるすべての可能性について」処理する必要がある match i{ // Cと違いbreakしなくてもいい。ほかの式が実行されることはない 3 => println!("three"), // 複数の指揮を実行するときは{}で括る 4 => { println!("four"); println!("FOUR") }, // 取りうるすべての可能性を記述しなければいけないため // ワイルドカード _ を使ってCのdefaultと同じことをする。 _ => println!("others"), }; }
matchの結果をそのまま変数に渡すこともできる。
fn main() { let i = 3; let msg:&str = match i{ 3 => "three", 4 => "four", _ => "others", }; println!("{}",msg); }
// 入出力を使う use std::io; fn main() { // 変数 buffer を定義 let mut buffer = String::new(); // bufferへ文字列入力 io::stdin().read_line(&mut buffer); // bufferを表示 println!("{}",buffer); }
// 入出力を使う use std::io; fn main() { // 変数 buffer を定義 let mut buffer = String::new(); io::stdin().read_line(&mut buffer); //標準入力から入力 // 型指定 :i32 が必要 // read_lineから読み込んだ文字列に対してはtrim()が必要 let val:i32 = buffer.trim().parse().unwrap(); // bufferを表示 println!("{}",val); }
// ファイル入力を使う use std::{fs::File, io::Read}; fn main() { let filename = "test.txt";
// ファイル入力を使う let mut file = File::open(filename).expect("file not found"); // 変数 buffer を定義 let mut buffer = String::new(); // ファイルから入力 file.read_to_string(&mut buffer).expect("something went wrong reading the file"); // ファイルを閉じる方法 // 方法1 スコープを抜けるとdropが呼ばれる(C++のデストラクタのようなもの) // 方法2 明示的にdropを呼び出す drop(file); // 明示的にdropを呼び出す // bufferを表示 println!("{}",buffer); }
use std::fs::File; // write!やwrite_allを使うのに必要 use std::io::Write; // 戻り値は std::io::Result<()> 型 // 最後に OK(()) を返す。 // これを入れないとwrite_allなどが使えない fn main()-> std::io::Result<()> { let filename = "test.txt"; // ファイル入力を使う let mut file = File::create(filename)?; // 末尾の ? は例外処理のようなもの関連 // ただしRustには例外がないのであくまでそれっぽく振る舞うだけ。 // 変数 buffer を定義 let _buffer = "hello".to_string(); // ファイルへ書き出し file.write_all(_buffer.as_bytes())?; // こっちも使える // write!(file,"hello2")?; // ファイルを閉じる drop(file); // std::io::Result<()> 型の場合 Ok(()) }
CargoはRustのビルドシステム及びパッケージマネージャ。
cargo buildはカレントディレクトリにsrcディレクトリがあることを想定する。
デバッグビルド
リリースビルド
Rustではパッケージのことを「クレート」というらしい。Cargo.tomlの[dependencies]にモジュール名とバージョンを記述する。
[package]
name = "myproj"
version = "0.1.0"
edition = "2021"
[dependencies]
regex = "1.6.0"
use regex::Regex; fn main() { let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap(); println!("Did our date match? {}", re.is_match("2014-01-01")); }
cargo buildすると勝手にcrates.ioからクレートを取得するので上記の記述以外にすることはない。
VSCodeでデバッグできるのだが、私の環境では、クレートを使っているとVSCode関連で失敗する。
細かく検証していないが上記regexを使った場合VSCodeからは実行すらできない。PowerShellからは動く。
どれだけ検索しても対策が見つからなかったがcodelldbのバージョンによっては起こる?らしい。