#はじめに
この記事はこちらの記事の続きです。
#プラグインをGUIに拡張する
前回プラグインのロジック部分が完成したので、今回はそこにUIを付け加えていきます。
##設計
UIを実装するにあたって、まずはどんなウィジェット(ボタン、チェックボックス、etc)をどのように配置するのかを大まかに考えておく必要があります。
そこで今回作成するプラグインの仕様をもう一度確認しましょう。
ノートを指定したスケール(音階)にスナップさせる
前回より
スケールというのは基本的にはいくつかが既に存在していて、ユーザーが新しく作り出すということは滅多にありません。多分。
ということは、Googleの検索ボックスのようなTextInput
によってスケールを決定するよりも、リストのようなものから限られたスケールを選択する方が良さそうです。
また画面下部にRun
ボタンを配置して、それによって最終的なスケールの決定と変更の反映を行うようにしましょう。
##技術構成を考える
設計が大まかに定まったので早速コーディングしたいところですが、残念ながらRustの標準ライブラリだけではGUIを作成することは非常に困難です。
よって、GUIを作成するにあたってどのようなライブラリを使用するのかを決める必要があります。
RustにどんなGUIライブラリがあるのかはこちらの記事がとても参考になりました。
記事では
- azul
- conrod
- gtk
- iced
- OrbTk
が紹介されていました。
その中でも今回はicedを使っていこうと思います。
icedとは、Elmライクなアーキテクチャを持つ型安全なGUIライブラリです。
また先の記事のexampleの大部分ではトレイトによる継承(?)のような実装がされていました。
これにより実装しなければいけない関数が明確になり、完成までの道のりがはっきりするのもいいですね。
##実装
設計と技術構成が決まったところで、いよいよ実装していきます。
そしてできたものがこちら。
###ソースコード
src/main.rs
#![windows_subsystem="windows"]
mod even_scale;
mod app;
mod style;
use std::process;
use iced::{Application,Settings};
const WINDOW_WIDTH: u32=640;
const WINDOW_HEIGHT: u32=480;
fn main(){
let window_setting=iced::window::Settings{
size: (WINDOW_WIDTH,WINDOW_HEIGHT),
min_size: None,
max_size: None,
resizable: false,
decorations: false,
transparent: true,
always_on_top: false,
icon: None,
};
let settings=Settings{
window: window_setting,
default_font: Some(include_bytes!("../fonts/NotoSansJP-Bold.otf")),
..Settings::default()
};
if let Err(err)=app::State::run(settings){
eprint!("Error:{}\n",err);
process::exit(1);
}
}
src/app.rs
use std::process;
use iced::{Align,Application,button,Button,Clipboard,Color,Column,Command,Container,Element,executor,Font,HorizontalAlignment,image,Image,Length,pick_list,PickList,Text};
use utau_rs::*;
use super::{even_scale::*,style::*};
#[derive(Default)]
pub struct State{
selected_scale: Option<Scale>,
uta_sections: UtaSections,
pick_list: pick_list::State<Scale>,
run: button::State,
exit: bool,
}
#[derive(Clone,Debug)]
pub enum Message{
ScaleSelect(Scale),
Run,
}
impl Application for State{
type Executor=executor::Default;
type Message=Message;
type Flags=();
fn new(_flags: ())->(Self,Command<Self::Message>){
(Self::default(),Command::none())
}
fn title(&self)->String{
String::from("even scale")
}
fn update(&mut self,message: Self::Message,_clipboard: &mut Clipboard)->Command<Self::Message>{
match message{
Message::ScaleSelect(scale)=>self.selected_scale=Some(scale),
Message::Run=>{
let selected_scale=match self.selected_scale{
Some(some)=>some,
None=>{
self.exit=true;
return Command::none();
}
};
if let Err(err)=even_scale(&mut self.uta_sections,selected_scale){
eprint!("Error:{}\n",err);
process::exit(1);
}
if let Err(err)=self.uta_sections.write(){
eprint!("Error:{}\n",err);
process::exit(1);
}
self.exit=true;
}
}
Command::none()
}
fn view(&mut self)->Element<Message>{
let comment_text=Text::new("↓choose scale that you want↓")
.font(Font::External{name: "BRADHITC",bytes: include_bytes!("../fonts/karakaze-R.otf")})
.size(30)
.horizontal_alignment(HorizontalAlignment::Center);
let scale_list=PickList::new(
&mut self.pick_list,
&Scale::ALL[..],
self.selected_scale,
Message::ScaleSelect,
)
.text_size(20)
.style(PickList);
let image=Container::new(Image::new(image::Handle::from_path("resource/background0.png")))
.align_x(Align::Center)
.align_y(Align::Center);
let run_button=Button::new(
&mut self.run,
Text::new("Run")
.size(30)
.horizontal_alignment(HorizontalAlignment::Center)
)
.width(Length::Shrink)
.height(Length::Shrink)
.min_width(80)
.style(Button)
.on_press(Message::Run);
let contents=Column::new()
.align_items(Align::Center)
.push(comment_text)
.spacing(10)
.push(scale_list)
.push(image)
.push(run_button);
Container::new(contents)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.style(Container)
.into()
}
fn background_color(&self)->Color{
Color::TRANSPARENT
}
fn should_exit(&self)->bool{
self.exit
}
}
src/style.rs
use iced::{Background,button,Color,container,pick_list};
const CONTAINER_R: u8=0x36;
const CONTAINER_G: u8=0x39;
const CONTAINER_B: u8=0x3F;
const BUTTON_R: u8=0xFF;
const BUTTON_G: u8=0x65;
const BUTTON_B: u8=0x1C;
const TEXT_R: u8=0xDE;
const TEXT_G: u8=0xDE;
const TEXT_B: u8=0xDE;
pub struct PickList;
impl pick_list::StyleSheet for PickList{
fn menu(&self)->pick_list::Menu{
pick_list::Menu::default()
}
fn active(&self)->pick_list::Style{
pick_list::Style{
text_color: Color::from_rgb8(CONTAINER_R,CONTAINER_G,CONTAINER_B),
icon_size: 0.5,
..pick_list::Style::default()
}
}
fn hovered(&self)->pick_list::Style{
pick_list::Style{
border_color: Color::BLACK,
..self.active()
}
}
}
pub struct Button;
impl button::StyleSheet for Button{
fn active(&self)->button::Style{
button::Style{
background: Some(Background::Color(Color::from_rgb8(BUTTON_R,BUTTON_G,BUTTON_B))),
border_radius: 10.0,
text_color: Color::from_rgb8(CONTAINER_R,CONTAINER_G,CONTAINER_B),
..button::Style::default()
}
}
fn hovered(&self)->button::Style{
button::Style{
border_width: 1.0,
border_color: Color::WHITE,
..self.active()
}
}
fn pressed(&self)->button::Style{
let active=self.active();
let hovered=self.hovered();
button::Style{
background: Some(Background::Color(active.text_color)),
border_width: hovered.border_width,
border_color: hovered.border_color,
text_color: match active.background{
Some(Background::Color(some))=>some,
None=>panic!("Error<{}:{}>:不明なエラーが発生しました.\n",file!(),line!()),
},
..active
}
}
}
pub struct Container;
impl container::StyleSheet for Container{
fn style(&self)->container::Style{
container::Style{
text_color: Some(Color::from_rgb8(TEXT_R,TEXT_G,TEXT_B)),
background: Some(Background::Color(Color::from_rgb8(CONTAINER_R,CONTAINER_G,CONTAINER_B))),
border_radius: 10.0,
..container::Style::default()
}
}
}
src/even_scale.rs
use utau_rs::*;
#[allow(non_camel_case_types)]
#[derive(Clone,Copy,Debug,Eq,PartialEq)]
pub enum Scale{
C,C_sharp,Cm,Cm_sharp,
D,D_sharp,Dm,Dm_sharp,
E,E_sharp,Em,Em_sharp,
F,F_sharp,Fm,Fm_sharp,
G,G_sharp,Gm,Gm_sharp,
A,A_sharp,Am,Am_sharp,
B,B_sharp,Bm,Bm_sharp,
}
impl Scale{
pub const ALL: [Scale;28]=[
Scale::C,Scale::C_sharp,Scale::Cm,Scale::Cm_sharp,
Scale::D,Scale::D_sharp,Scale::Dm,Scale::Dm_sharp,
Scale::E,Scale::E_sharp,Scale::Em,Scale::Em_sharp,
Scale::F,Scale::F_sharp,Scale::Fm,Scale::Fm_sharp,
Scale::G,Scale::G_sharp,Scale::Gm,Scale::Gm_sharp,
Scale::A,Scale::A_sharp,Scale::Am,Scale::Am_sharp,
Scale::B,Scale::B_sharp,Scale::Bm,Scale::Bm_sharp,
];
}
impl Default for Scale{
fn default()->Self{
Scale::C
}
}
impl std::fmt::Display for Scale{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{}",match self{
Scale::C=>"C",Scale::C_sharp=>"C#",Scale::Cm=>"Cm",Scale::Cm_sharp=>"C#m",
Scale::D=>"D",Scale::D_sharp=>"D#",Scale::Dm=>"Dm",Scale::Dm_sharp=>"D#m",
Scale::E=>"E",Scale::E_sharp=>"E#",Scale::Em=>"Em",Scale::Em_sharp=>"E#m",
Scale::F=>"F",Scale::F_sharp=>"F#",Scale::Fm=>"Fm",Scale::Fm_sharp=>"F#m",
Scale::G=>"G",Scale::G_sharp=>"G#",Scale::Gm=>"Gm",Scale::Gm_sharp=>"G#m",
Scale::A=>"A",Scale::A_sharp=>"A#",Scale::Am=>"Am",Scale::Am_sharp=>"A#m",
Scale::B=>"B",Scale::B_sharp=>"B#",Scale::Bm=>"Bm",Scale::Bm_sharp=>"B#m",
})
}
}
pub struct KeyTone([u32;7]);
const C: u32=24;
const D: u32=26;
const E: u32=28;
const F: u32=29;
const G: u32=31;
const A: u32=33;
const B: u32=35;
impl KeyTone{
pub fn new(scale: &Scale)->Result<KeyTone,&'static str>{
Ok(match scale{
Scale::C |Scale::Am =>KeyTone([C,D,E,F,G,A,B]),
Scale::C_sharp|Scale::Am_sharp=>KeyTone([C+1,D+1,E+1,F+1,G+1,A+1,B+1]),
Scale::D |Scale::Bm =>KeyTone([C+1,D,E,F+1,G,A,B]),
Scale::D_sharp|Scale::Cm =>KeyTone([C,D,E-1,F,G,A-1,B-1]),
Scale::E |Scale::Cm_sharp=>KeyTone([C+1,D+1,E,F+1,G+1,A,B]),
Scale::F |Scale::Dm =>KeyTone([C,D,E,F,G,A,B-1]),
Scale::F_sharp|Scale::Dm_sharp=>KeyTone([C+1,D+1,E+1,F+1,G+1,A+1,B]),
Scale::G |Scale::Em =>KeyTone([C,D,E,F+1,G,A,B]),
Scale::G_sharp|Scale::Fm =>KeyTone([C,D-1,E-1,F,G,A-1,B-1]),
Scale::A |Scale::Fm_sharp=>KeyTone([C+1,D,E,F+1,G+1,A,B]),
Scale::A_sharp|Scale::Gm =>KeyTone([C,D,E-1,F,G,A,B-1]),
Scale::B |Scale::Gm_sharp=>KeyTone([C+1,D+1,E,F+1,G+1,A+1,B]),
_=>return Err("不明なエラーが発生しました."),
})
}
fn unwrap(&self)->[u32;7]{
match self{
&KeyTone(some)=>some,
}
}
}
pub fn even_scale(uta_sections: &mut UtaSections,scale: Scale)->Result<(),&'static str>{
let tones=KeyTone::new(&scale).unwrap();
for section in uta_sections.sections.iter_mut(){
if tones.unwrap().iter().all(|&x|(x%section.note_num)!=0){
let mut near=section.note_num as i32-tones.unwrap()[0] as i32;
for tone in tones.unwrap(){
let tone=match section.note_num{
24..=35=>tone+12*0,
36..=47=>tone+12*1,
48..=59=>tone+12*2,
60..=71=>tone+12*3,
72..=83=>tone+12*4,
84..=95=>tone+12*5,
96..=107=>tone+12*6,
_=>return Err("不明なエラーが発生しました."),
};
if (section.note_num as i32-tone as i32).abs()<near{
near=(section.note_num as i32-tone as i32).abs();
}
}
section.note_num=(section.note_num as i32+near) as u32;
}
};
Ok(())
}
疲れました(本音
気をとりなしてコードの説明をしていきます。
今回は前回と違い、icedの説明も交えながらいきます。
###解説
src/main.rs
#![windows_subsystem="windows"]
DOS窓を表示しないように設定します。
ちなみにDOS窓というのは、いわゆるコマンドプロンプトみたいなウィンドウのことを指します。
これをしないと、プラグインを実行したときに空黒空白のDOS窓とGUIの二つのウィンドウが表示されることになります。
let window_setting=iced::window::Settings{/*省略*/}
ウィンドウの設定情報を変数に束縛します。
今回ウィンドウを伸縮不可のタイルライクなものにしたかったので、size
とresizable
とdecorations
を設定していきます。
let settings=Settings{/*省略*/}
実行に必要な設定情報を変数に束縛します。
今回はicedで日本語表示を可能にするためにデフォルトで使用するフォントを指定し、先程指定したウィンドウについての設定を追加し、あとの設定は..Settings::default()
で片づけます。
..○○::default()
ってめちゃくちゃ便利ですね。びっくりしました。
if let Err(err)=app::State::run(settings){/*省略*/}
先程の設定情報をもとにプラグインを実行します。
icedを用いたGUIアプリケーションでは、main()
は基本的にこの実行のみを請け負います。
後述するshould_exit()
次第ではrun()
の後に他の処理を実装することも可能ですので、ケースバイケースでいきましょう。
src/app.rs
#[derive(Default)]
pub struct State{/*省略*/}
各ウィジェットの状態を格納する変数や実行に関連する変数を、ひとつのオブジェクトとして定義します。
#[derive(Default)]
でDefault
トレイトを実装しておくと便利です。
しかし型の中には手動でDefault
トレイトを実装しなければならないものもあるので注意しましょう。
#[derive(Clone,Debug)]
pub enum Message{/*省略*/}
ボタンを押したときやリストを選択したときなどに送受信されるMessage
列挙体を定義します。
Clone
トレイトやDebug
トレイトは後の関数で要求されるため、derive
で実装する必要があります。
impl Application for State{/*省略*/}
Application
トレイトをState
構造体に実装します。
この実装が完了するとState::run()
を使用できるようになります。
ちなみにこのApplication
トレイトに似たものにSandbox
トレイトがありますが、これはApplication
トレイトの機能制限版になります。
Application
トレイトよりも軽量な半面、should_exit()
のような関数は実装できません。
はじめはSandbox
トレイトで実装を進め、必要があればApplication
トレイトに移行しましょう。
IDEにもよりますが、移行は関数の引数と返り値を若干変えるだけなので2、3分程度で完了します。
fn new(_flags: ())->(Self,Command<Self::Message>){/*省略*/}
State
の初期値を定義します。
ここで返り値は(Self,Command<Self::Message>)
というタプルになりますが、このCommand<Self::Message>
は非同期処理用のものです。
今回非同期処理を実装する必要はないので、Command::none()
を返します。
ちなみに他の非同期処理用の関数にはsubscription()
があります。
Command
とsubscription()
の詳しい説明については以下の記事が参考になりました。
https://sukawasatoru.com/docs/2021-02-13-iced
fn update(&mut self,message: Self::Message,_clipboard: &mut Clipboard)
->Command<Self::Message>{/*省略*/}
後述のview()
等で何らかのMessage
が送られたときに任意の処理を実行します。
基本的には
match message{
Message::ScaleSelect(scale)=>/*省略*/,
Message::Run=>/*省略*/,
}
上記のように、引数のmassage
に対してパターンマッチングを行い、各Message
についての処理を実装します。
今回はScaleSelect(scale)
の場合にはself.selected_scale=scale
をし、Run
の場合にはeven_scale()
と.write()
を行うようにしました。
fn view(&mut self)->Element<Message>{/*省略*/}
ウィンドウにウィジェットやテキストをどのように配置するのかを定義します。
自分なりのicedでウィジェットをいい感じにコーディングするためのコツですが、まずウィジェット毎に変数を用意するといいと思います。
この時点で.style()
や.size()
のようなメソッドは付けておくと、次のColumn::new()
で.push()
するだけでよくなります。
またColumn::new()
では.spacing()
などのメソッドでウィジェット間の縦方向のスペースを決定することができます。
ウィジェットを横に並べたい場合には
.push(
Row::new()
.push(some_widget)
)
上記をColumn::new()
のメソッドチェーンに付け加えます。
一通りColumn::new()
にメソッドを付け加えたら、それを束縛する変数を引数としてContainer::new(some_contents)
をします。
これはview()
の返り値となるので、;
は付けません。
またContainer::new()
のメソッドチェーンに
Container::new(contents)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.style(Container)
.into()
上記を加えておくと、ウィジェットが画面中央に配置されて見栄えがよくなります。
.style()
は後述のcontainer::StyleSheet
トレイトを実装しているならば付けておきましょう。
メソッドチェーンの最後に.into()
を付け加えるのも忘れずに。
fn should_exit(&self)->bool{
self.exit
}
self.exit
の値によって、run()
を終了するべきかそうでないかを判断します。
update()
等でself.exit
にtrue
を代入することでrun()
を終えつつmain()
は続かせるのが主な使い道だと思います。
src/style.rs
impl pick_list::StyleSheet for PickList{/*省略*/}
pick_list
のスタイルを設定します。
アクティブ時の場合やホバー時の場合など、ウィジェットの状態別にスタイルを設定できます。
icedのCSSのようなものだと思っていただいて多分間違いないでしょう。
またここでStyleSheet
を実装した構造体を先述の.style()
の引数とすることで、そのウィジェットにスタイルを適用することができます。
button
、container
についても同様の説明ができるので省略します。
#完成!!!
長い戦いでした。
GUI作成自体が初めてであるとはいえ、結構完成までに遠回りをしました。
最後に完成品のスクショを貼っておきます。
本当は五線譜のところで選択したスケールの構成音を表示したりとかもしたかったのですが、流石にそこまでする気力は残っていませんでした。
これはまた次の機会のアイディアとして残しておこうと思います。
#おわりに
API作成編からGUI作成編までの三部構成だった記事もようやっと終わりです。
初めての記事執筆でしたが、自分の理解していることの再確認をするにはもってこいの場でした。
この記事でRustでUTAUプラグインを作成する方が一人でも増えたり、icedの実装で困っている方が少しでも解決へと繋がってくれたら、本当に嬉しい限りです。
また何かあればQiitaに記事を投稿するつもりです。
その時はまたよしなに(?)していただければと思います。
またね。