折角C++/WinRTなのにXAML UIならではのことをやってなかったので、やってみようと思ったのだった。
C++とXAMLスクリプトだけの単体exeアプリでも、これくらいは出来るよという感じで。
(C#/WPFとかだともっと簡単だよというのはナシ!あくまでWin32ネイティブで実現させることにこだわる。)
-
アクリル素材
すりガラスのような半透明の背景で、多分Windows 10 バージョン1903以降が対象。
「背景アクリル」と「アプリ内アクリル」の2種類あるが、今回試すのは背景アクリルの方。
背景アクリルを適用したコントロールは、アプリ内で裏に別の物体があろうがなかろうが、全部すっ飛ばして後ろの別ウインドウや背景が透けて見えるようになる。
-
リップルボタン
クリックするとそこから波紋が広がる効果の付いたボタン。
本来であればクリックした位置から波紋が発生すべきなんだろうが、それをしようと思ったらコードが煩雑になりそうだったので、今回のサンプルではクリックの位置に関わらずボタンの中央から波紋が出るにとどめた。なのでリップル「っぽい」ボタン。
参考GIF動画
こんなの。
サンプルコード
例によってVSCodeでビルドする前提なので、VSCodeとBuildToolsの記事で基本的な環境を作りつつ、C++/WinRTの記事でWinRTならではの環境構築をしていただければと思う。
そのうえで下記のソース一式を用意してビルドすれば動くはず。
<Project DefaultTargets="Build" ToolsVersion="16.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="packages\Microsoft.Windows.CppWinRT.2.0.210225.3\build\native\Microsoft.Windows.CppWinRT.props"/>
<ItemGroup>
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.default.props"/>
<PropertyGroup>
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v142</PlatformToolset>
<WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion>
<CharacterSet>Unicode</CharacterSet>
<IntermediateOutputPath>obj\$(Configuration)\$(Platform)\</IntermediateOutputPath>
<OutDir>bin\$(Configuration)\$(Platform)\</OutDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props"/>
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
<Manifest>
<AdditionalManifestFiles>app.manifest;%(AdditionalManifestFiles)</AdditionalManifestFiles>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<GenerateDebugInformation>false</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="*.cpp" Exclude="pch.cpp"/>
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="*.h"/>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="*.rc"/>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Targets"/>
<Import Project="packages\Microsoft.Windows.CppWinRT.2.0.210225.3\build\native\Microsoft.Windows.CppWinRT.targets"/>
</Project>
プロジェクトファイル。
あ、最初と最後らへんでImport
してる「Microsoft.Windows.CppWinRT」のパス(に含まれるバージョン 2.0.210225.3)は、導入したパッケージに合わせるよう気を付けて。
<?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>
おまじないマニフェスト。
//
#define IDR_XAML_MAIN 101
リソースのヘッダー。
#include "resource.h"
IDR_XAML_MAIN XAMLFILE "main.xaml"
リソーススクリプト。今回もXAML1個。
#pragma once
#include <windows.h>
#include <windows.ui.xaml.hosting.desktopwindowxamlsource.h> //DesktopWindowXamlSource
//名称かぶり対策(前半)ここから
#pragma push_macro("GetCurrentTime")
#pragma push_macro("TRY")
#undef GetCurrentTime
#undef TRY
//名称かぶり対策(前半)ここまで
#include <winrt/windows.ui.xaml.hosting.h> //WindowsXamlManager
#include <winrt/windows.ui.xaml.markup.h> //XamlReader
#include <winrt/windows.ui.xaml.controls.h>
#include <winrt/windows.ui.xaml.media.h> //VisualTreeHelper
#include <winrt/windows.foundation.h> //IInspectable
#include <winrt/windows.security.cryptography.h> //CryptographicBuffer
//名称かぶり対策(後半)ここから
#pragma pop_macro("TRY")
#pragma pop_macro("GetCurrentTime")
//名称かぶり対策(後半)ここまで
プリコンパイル済みヘッダー。
#include "pch.h"
プリコンパイル済みヘッダーを生成するためのcpp。
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Background="{ThemeResource SystemControlChromeHighAcrylicWindowMediumBrush}">
<Grid.Resources>
<Style x:Key="buttonStyle" TargetType="Button">
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid x:Name="buttongrid" Background="Gray">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver">
<Storyboard>
<ColorAnimation Storyboard.TargetName="buttongrid" Storyboard.TargetProperty="(Background).(Color)" To="Blue" Duration="0:0:0.1"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ColorAnimation Storyboard.TargetName="buttongrid" Storyboard.TargetProperty="(Background).(Color)" To="Red" Duration="0"/>
<DoubleAnimation Storyboard.TargetName="elps_scale" Storyboard.TargetProperty="ScaleX" From="0" To="8.0" Duration="0:0:0.5"/>
<DoubleAnimation Storyboard.TargetName="elps_scale" Storyboard.TargetProperty="ScaleY" From="0" To="8.0" Duration="0:0:0.5"/>
<DoubleAnimation Storyboard.TargetName="ellipse" Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:0.3">
<DoubleAnimation.EasingFunction>
<ExponentialEase Exponent="2" EasingMode="EaseIn"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</VisualState>
<VisualStateGroup.Transitions>
<VisualTransition From="Pressed" GeneratedDuration="0:0:0.5"/>
<VisualTransition From="PointerOver" To="Normal" GeneratedDuration="0:0:0.2"/>
</VisualStateGroup.Transitions>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Ellipse x:Name="ellipse" Width="20" Height="20" Fill="White" RenderTransformOrigin="0.5, 0.5" Opacity="0">
<Ellipse.RenderTransform>
<ScaleTransform x:Name="elps_scale" ScaleX="8.0" ScaleY="8.0"/>
</Ellipse.RenderTransform>
</Ellipse>
<ContentPresenter VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<StackPanel>
<Grid Height="25"/>
<Button Name="button1" Content="ボタン" Width="80" Height="25" Style="{StaticResource buttonStyle}"/>
</StackPanel>
</Grid>
XAMLファイル。
今回はちょっと長めだが、大部分がリップルボタンを作るための記述になってしまった。(レイアウト本体は実質4行。)
まず背景アクリルだが、アクリルにしたいコントロール(今回は一番外側のGrid
)のBackground
属性を{ThemeResource SystemControlChromeHighAcrylicWindowMediumBrush}
にする。
何とそれだけ!
ちなみにこのアクリルのテーマには、すけ具合や色味?によって種類があり、MS公式に一覧表がある無くなっている(2022年12月現在)が、一応テーマの定義はGitHubで公開されているので参考に。
背景アクリル・アプリ内アクリルと不透明度の違いは分かるけど、種類が多くて使いこなせる気がしない。
なお公式の説明によると標準の背景アクリルはSystemControlAcrylicWindowBrush
(不透明度80%)のようだが、今回はスケスケ具合が分かりやすいように、より透けるSystemControlChromeHighAcrylicWindowMediumBrush
(不透明度60%)をチョイスしている。
そしてリップルボタンだが、<ControlTemplate>
内の<VisualStateManager.VisualStateGroups>
の中身がキモ。
<VisualState>
ってのがボタンの状態を表していて、Pressed
がクリックした時のことなので一番中身が多い。
ボタンの背景色を直ちにRed
にする & 仕込んでおいたEllipse
を横も縦も0.5秒かけて8倍まで拡大する & Ellipse
の不透明度を0.3秒かけて1から0にする(つまりだんだん消えていく)処理を詰め込んである。
さらに不透明度は均等に減っていくのではなく、ExponentialEase
というのをEaseIn
モードで使って、最初はゆっくり薄くなるが終盤急に消えるような感じにしている。
本当は波紋がボタンの外にはみ出ない処理もXAMLスクリプト内で完結したかったのだが、クリッピング領域に使うRect
の要素が上手くバインディング出来なかったため断念し、泣く泣くcppの方に書いたのであった。
#include "pch.h"
#include "resource.h"
using namespace winrt;
using namespace Windows::UI::Xaml; //XamlRoot
using namespace Windows::UI::Xaml::Hosting; //WindowsXamlManager
using namespace Windows::UI::Xaml::Markup; //XamlReader
using namespace Windows::UI::Xaml::Controls;
using namespace Windows::UI::Xaml::Media; //VisualTreeHelper
using namespace Windows::Foundation; //IInspectable
using namespace Windows::Security::Cryptography; //CryptographicBuffer
//名前からコントロールを特定するテンプレート(今回は未使用)
template<typename T> T Element(const wchar_t *name) {
return _xamlroot.Content().as<Panel>().FindName(name).as<T>();
};
LRESULT CALLBACK WindowProc(HWND, UINT, WPARAM, LPARAM);
void InitElement(DependencyObject const &);
void OnButtonLoaded(IInspectable const &, RoutedEventArgs const &);
HWND _hWnd;
HWND _hWndXamlIsland = nullptr;
XamlRoot _xamlroot = { nullptr };
int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd) {
//ウインドウクラス登録
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(nullptr, IDI_APPLICATION);
if (::RegisterClassEx(&windowClass) == 0) {
::MessageBox(nullptr, L"Windows registration failed!", L"Error", MB_OK);
return 0;
}
//ウインドウ作成
_hWnd = ::CreateWindow(
szWindowClass,
L"Windows c++ Win32 Desktop App",
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT, CW_USEDEFAULT, 320, 640,
nullptr,
nullptr,
hInstance,
nullptr
);
if (_hWnd == nullptr) {
::MessageBox(nullptr, L"Call to CreateWindow failed!", L"Error", MB_OK);
return 0;
}
//C++/WinRTのおまじない
init_apartment(apartment_type::single_threaded);
WindowsXamlManager winxamlmanager = WindowsXamlManager::InitializeForCurrentThread();
//XAMLホスト準備
DesktopWindowXamlSource desktopSource;
auto interop = desktopSource.as<IDesktopWindowXamlSourceNative>();
check_hresult(interop->AttachToWindow(_hWnd));
interop->get_WindowHandle(&_hWndXamlIsland);
//XAMLホストのサイズをメインウインドウに合わせる
RECT rcClient;
::GetClientRect(_hWnd, &rcClient);
::SetWindowPos(_hWndXamlIsland, nullptr, 0, 0, rcClient.right - rcClient.left, rcClient.bottom - rcClient.top, SWP_SHOWWINDOW);
//リソースからXAMLコードを取り出してXAMLホストにセット
auto hResInfo = ::FindResource(hInstance, MAKEINTRESOURCE(IDR_XAML_MAIN), L"XAMLFILE");
auto res_size = ::SizeofResource(hInstance, hResInfo);
std::vector<uint8_t> xaml_array(res_size, 0);
auto hGlobal = ::LoadResource(hInstance, hResInfo);
char *xaml_bin = (char *)::LockResource(hGlobal);
::memcpy_s(xaml_array.data(), res_size, xaml_bin, res_size);
auto xaml_buf = CryptographicBuffer::CreateFromByteArray(xaml_array);
auto xamlstr = CryptographicBuffer::ConvertBinaryToString(BinaryStringEncoding::Utf8, xaml_buf);
auto xamlContainer = XamlReader::Load(xamlstr).as<Panel>();
desktopSource.Content(xamlContainer);
_xamlroot = xamlContainer.XamlRoot();
//コントロールの初期化
InitElement(xamlContainer);
//メインウインドウ表示
::ShowWindow(_hWnd, nShowCmd);
::UpdateWindow(_hWnd);
//メッセージループ
MSG msg = {};
while (::GetMessage(&msg, nullptr, 0, 0)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
return 0;
}
//ウインドウプロシージャ
LRESULT CALLBACK WindowProc(HWND hWnd, UINT messageCode, WPARAM wParam, LPARAM lParam) {
RECT rcClient;
switch (messageCode) {
case WM_CREATE:
//return 0;
break;
case WM_DESTROY:
::PostQuitMessage(0);
break;
case WM_SIZE: //メインウインドウのサイズが変わったらXAMLホストも追従
::GetClientRect(hWnd, &rcClient);
::SetWindowPos(_hWndXamlIsland, nullptr, 0, 0, rcClient.right - rcClient.left, rcClient.bottom - rcClient.top, SWP_SHOWWINDOW);
return 0;
break;
default:
return ::DefWindowProc(hWnd, messageCode, wParam, lParam);
break;
}
return 0;
}
//コントロールの初期化処理
void InitElement(DependencyObject const &object) {
auto count = VisualTreeHelper::GetChildrenCount(object);
if (count <= 0) return;
for (int32_t i = 0; i < count; ++i) {
auto child = VisualTreeHelper::GetChild(object, i);
if (child.try_as<Button>() != nullptr) {
auto button = child.as<Button>();
button.Loaded({ &OnButtonLoaded });
}
InitElement(child);
}
}
//ボタン読み込み時のイベントハンドラ
void OnButtonLoaded(IInspectable const &sender, RoutedEventArgs const &e) {
auto button = sender.as<Button>();
Rect rect(0, 0, button.Width(), button.Height());
auto rectgeo = RectangleGeometry();
rectgeo.Rect(rect);
button.Clip(rectgeo);
}
アプリのメイン。
特筆すべきことはやってない…予定だったが、先述のクリッピング処理がXAMLスクリプト側で完結出来なかったせいでこっちに来た。
まずはInitElement
関数。
引数に最上位のコントロールを与えると、再帰的にすべての子孫コントロールを調べ上げる。
調べたやつがボタンだった場合は、Loaded
にOnButtonLoaded
をあてがう。
OnButtonLoaded
の方はというと、ボタンと同じ大きさのRect
(を持つRectangleGeometry
)を作ってそれをボタンのClip
に登録し、ボタンの中身(今回の場合は波紋)がボタンの外に出ないようにしている。
波紋がボタンの外に出るのもそれはそれで面白いんだけどね。派手で。