はじめに
JUCE の Timer クラスや Thread クラスを利用すると、タイマーやスレッドによる非同期処理を実装できます。
これらのクラスは便利なのですが、継承して使用することが前提の設計になっているため、タイマーやスレッドを利用する際に目的に合わせたクラスを毎回実装する必要があって、そこだけちょっと扱いにくいことがあります。
僕は最近その問題に対して、より手軽に使えるようにしたタイマーやスレッドのクラスを作って使っているので、今回はそれを紹介します。
クラス定義
/** 任意の処理をクラス外から設定できるタイマークラス
*/
class CustomizableTimer
: public juce::Timer
{
public:
/** タイマーによって呼び出されるコールバック。
このコールバックの引数には、タイマーを起動した CustomizableTimer 自身の参照が渡される。
@pre timerCallback() が呼び出されるよりも前に有効な関数がセットされること。
@note タイマーの実行中に別のスレッドからこの変数が書き換えられないように注意すること。
*/
std::function<void(CustomizableTimer &self)> onTimerCallback;
private:
void timerCallback() override
{
jassert(onTimerCallback != nullptr);
onTimerCallback(*this);
}
};
/** 任意の処理をクラス外から設定できるスレッドクラス
*/
class CustomizableThread
: public juce::Thread
{
public:
/** コンストラクタ
@param threadName スレッドに設定する名前
*/
CustomizableThread(juce::String threadName)
: juce::Thread(threadName)
{}
/** スレッド起動時に呼び出されるコールバック
このコールバックの引数には、スレッドを起動した CustomizableThread 自身の参照が渡される。
@pre startThread() を呼び出すより前に有効な関数をセットしておくこと。
@note スレッドの実行中にこの変数が書き換えられないように注意すること。
*/
std::function<void(CustomizableThread &self)> onRun;
private:
void run() override
{
try
{
jassert(onRun != nullptr);
onRun(*this);
}
catch (std::exception& e)
{
// スレッドの処理は例外で終了するべきではないので、
// ここではその例外を握りつぶし、デバッグ時にはアサーションで検知できるようにする。
jassert ("onRun should never throw." && false);
}
}
};
このように juce::Timer と juce::Thread を継承した小さなクラスを作り、そこに std::function クラスで外からコールバックを設定できるようにしています。
このようなクラスを使えば、以下のように、カスタムの処理を行う複数のタイマーやスレッドを手軽に扱えるようになります。
使用例
class MainComponent
: public juce::Component
{
public:
MainComponent()
{
// タイマーで実行する処理を設定する
timer1.onTimerCallback = [this](CustomizableTimer &) {
addMessage("timer1");
};
// タイマーで実行する処理を設定する
timer2.onTimerCallback = [this](CustomizableTimer &) {
addMessage("timer2");
};
// クラスの構築とともにタイマーを2つ起動する
timer1.startTimer(400);
timer2.startTimer(500);
// スレッドで実行する処理を設定する
thread1.onRun = [this](CustomizableThread &self) {
for( ; self.threadShouldExit() == false ; ) {
addMessage("thread1");
juce::Thread::sleep(600);
}
};
// スレッドで実行する処理を設定する
thread2.onRun = [this](CustomizableThread &self) {
for( ; self.threadShouldExit() == false ; ) {
addMessage("thread2");
juce::Thread::sleep(700);
}
};
// スレッドを2つ起動する
thread1.startThread();
thread2.startThread();
setSize(600, 400);
}
~MainComponent()
{
thread1.signalThreadShouldExit();
thread2.signalThreadShouldExit();
thread1.stopThread(-1); // スレッドの終了を待機
thread2.stopThread(-1); // スレッドの終了を待機
}
private:
CustomizableTimer timer1;
CustomizableTimer timer2;
CustomizableThread thread1 { "Thread1" };
CustomizableThread thread2 { "Thread2" };
// ...
};
サンプルコード
完全なサンプルコードを下に載せます。
このサンプルコードをビルドして実行すると、このような UI を持つウィンドウが表示され、タイマーやスレッドによってメッセージが追加されていきます。
このコードは JUCE の PIP 形式になっているため、このコードを *.h ファイルとして保存し Projucer で開けば、このプログラムをビルドするためのプロジェクトファイルが生成できます。(JUCE 6.0.4 で動作確認しています)
/*******************************************************************************
The block below describes the properties of this PIP. A PIP is a short snippet
of code that can be read by the Projucer and used to generate a JUCE project.
BEGIN_JUCE_PIP_METADATA
name: CustomizableTimerAndThreadDemo
version: 1.0.0
vendor: @hotwatermorning
website: https://diatonic.jp
description: Demo application of CustomizableTimer And CustomizableThread.
dependencies: juce_core, juce_data_structures, juce_events, juce_graphics,
juce_gui_basics
exporters: xcode_mac, vs2019, linux_make, xcode_iphone
type: Component
mainClass: MainComponent
useLocalCopy: 0
END_JUCE_PIP_METADATA
*******************************************************************************/
#pragma once
/** 任意の処理をクラス外から設定できるタイマークラス
*/
class CustomizableTimer
: public juce::Timer
{
public:
/** タイマーによって呼び出されるコールバック。
このコールバックの引数には、タイマーを起動した CustomizableTimer 自身の参照が渡される。
@pre timerCallback() が呼び出されるよりも前に有効な関数がセットされること。
@note タイマーの実行中に別のスレッドからこの変数が書き換えられないように注意すること。
*/
std::function<void(CustomizableTimer &self)> onTimerCallback;
private:
void timerCallback() override
{
jassert(onTimerCallback != nullptr);
onTimerCallback(*this);
}
};
/** 任意の処理をクラス外から設定できるスレッドクラス
*/
class CustomizableThread
: public juce::Thread
{
public:
/** コンストラクタ
@param threadName スレッドに設定する名前
*/
CustomizableThread(juce::String threadName)
: juce::Thread(threadName)
{}
/** スレッド起動時に呼び出されるコールバック
このコールバックの引数には、スレッドを起動した CustomizableThread 自身の参照が渡される。
@pre startThread() を呼び出すより前に有効な関数をセットしておくこと。
@note スレッドの実行中にこの変数が書き換えられないように注意すること。
*/
std::function<void(CustomizableThread &self)> onRun;
private:
void run() override
{
try
{
jassert(onRun != nullptr);
onRun(*this);
}
catch (std::exception& e)
{
// スレッドの処理は例外で終了するべきではないので、
// ここではその例外を握りつぶし、デバッグ時にはアサーションで検知できるようにする。
jassert ("onRun should never throw." && false);
}
}
};
class MainComponent
: public juce::Component
{
public:
MainComponent()
{
addAndMakeVisible(log);
log.setMultiLine(true);
log.setReadOnly(true);
// タイマーで実行する処理を設定する
timer1.onTimerCallback = [this](CustomizableTimer &) {
addMessage("timer1");
};
// タイマーで実行する処理を設定する
timer2.onTimerCallback = [this](CustomizableTimer &) {
addMessage("timer2");
};
// タイマーを2つ起動する
timer1.startTimer(400);
timer2.startTimer(500);
// スレッドで実行する処理を設定する
thread1.onRun = [this](CustomizableThread &self) {
for( ; self.threadShouldExit() == false ; ) {
addMessage("thread1");
juce::Thread::sleep(600);
}
};
// スレッドで実行する処理を設定する
thread2.onRun = [this](CustomizableThread &self) {
for( ; self.threadShouldExit() == false ; ) {
addMessage("thread2");
juce::Thread::sleep(700);
}
};
// スレッドを2つ起動する
thread1.startThread();
thread2.startThread();
setSize(600, 400);
}
~MainComponent()
{
thread1.signalThreadShouldExit();
thread2.signalThreadShouldExit();
thread1.stopThread(-1); // スレッドの終了を待機
thread2.stopThread(-1); // スレッドの終了を待機
}
private:
CustomizableTimer timer1;
CustomizableTimer timer2;
CustomizableThread thread1 { "Thread1" };
CustomizableThread thread2 { "Thread2" };
juce::TextEditor log;
void resized() override
{
auto b = getLocalBounds();
log.setBounds(b);
}
void addMessage(juce::String msg)
{
auto mm = juce::MessageManager::getInstance();
// 必ずメッセージスレッドで log 変数にメッセージを追加する。
// ただし、 callAsync での非同期処理実行時に
// すでに MainComponent が破棄されてしまっている可能性があるので、
// SafePointer でそれを判定する。
mm->callAsync([msg, sp = Component::SafePointer<MainComponent>(this), this] {
if(sp) {
log.moveCaretToEnd();
log.insertTextAtCaret(msg + "\n");
}
});
}
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};