この記事は
Julia Advent Calendar 20日目の記事です。
概要
Juliaは(作るのが)早くて(動くのが)速い素晴らしいプログラミング言語です。
最近では、JupyternotebookやJupyterlabをはじめ、Visual Studio CodeやPluto.jlと、いろいろな環境で動かすことができるので、誰でも簡単に試すことができます。
また、上記環境の構築は比較的容易なため、作ったスクリプトを配布して使ってもらうことも簡単だと思います。
でも、世の中にはGUIアプリにして渡さないとダメな場合があります。そんな時に便利なGUIアプリの配布方法として、CImGui.jlで作ったアプリケーションをPackageCompiler.jlで配布する方法を紹介します。
なぜ、CImGui.jlなのか
JuliaでGUIアプリを作る場合、Gtk.jlが便利だと思っていました。以下のような記事を参考にすれば、GUIアプリを簡単に作ることができます。
また、Gladeというツールを使えば、画面の作成もGUIで作ることができます。しかし、Gladeを使った場合には、PackageCompiler.jlでうまく一緒にビルドできないみたいだったので、少し嫌になりました。Plots.jlもうまくビルドできないし、そもそも生成されたファイルの容量が1.5GBを超えるようになり、とても辛い状況に・・・。
そんなときに、今年のJuliaConのPackageCompiler.jlの説明の動画を見てみると、CImGui.jlで作ったアプリを使って、実行可能なアプリ生成の説明をしていました。CImGuiはC++のイケてるGUIライブラリDear ImGuiのwrapperであるcimguiのJulia版のようです。特徴は、実装が簡単で、しかも見た目がかっこよく、しかもPackageCompilerを使って生成されるファイルも比較的軽量のようです!
本当に、簡単にPackageCompiler.jlでビルドできるのか?と思い試してみたところ、とても簡単だったため、本記事ではCImGui.jlで作ったアプリをPackageCompiler.jlでビルドする手順を、簡単に記載してみます。
作業手順
動かすことが目的のため、心を無にして、どんどん進めていきます。まずは、PackageCompiler.jlとCImGui.jlをaddしておいてください。2020/12/20現在、ImPlot.jlはaddしてはいけません。以下のような感じですね。
] add PackageCompiler CImGui
PackageCompiler.jlでビルドするためのPackageを作成する
PackageCompiler.jlでビルドするためには、そのためのパッケージを作成する必要があります。
パッケージの作成のための記事としては、例えば以下のような記事が大変役に立つと思います。
Juliaのプロジェクトと環境
まず、パッケージを作りたいフォルダに、以下のコマンドで移動します。ここでは、julialibというフォルダをあらかじめ作成しておき、そこに移動しています。
; cd "c:/julialib"
次に、以下のコマンドを実行して、パッケージを新規作成します。ここでは、GuiTestとしました。
] generate GuiTest
次に、以下のようにパッケージをactivateして、今回はCImGui.jlとPrintf.jlだけをaddします。
] activate GuiTest
] add CImGui Printf
これで、パッケージは完成です。
CImGui.jlのデモコードを、PackageCompiler.jlでビルドできる形に変形する
今回は、CImGui.jlのデモコード、具体的にはこちらのコードをベースに、PackageCompiler.jlでビルドできる形に変更します。生成されたGuiTest.jlを、以下内容に変更すれば良いです。
module GuiTest
using CImGui
using CImGui.CSyntax
using CImGui.CSyntax.CStatic
using CImGui.GLFWBackend
using CImGui.OpenGLBackend
using CImGui.GLFWBackend.GLFW
using CImGui.OpenGLBackend.ModernGL
using Printf
@static if Sys.isapple()
# OpenGL 3.2 + GLSL 150
const glsl_version = 150
GLFW.WindowHint(GLFW.CONTEXT_VERSION_MAJOR, 3)
GLFW.WindowHint(GLFW.CONTEXT_VERSION_MINOR, 2)
GLFW.WindowHint(GLFW.OPENGL_PROFILE, GLFW.OPENGL_CORE_PROFILE) # 3.2+ only
GLFW.WindowHint(GLFW.OPENGL_FORWARD_COMPAT, GL_TRUE) # required on Mac
else
# OpenGL 3.0 + GLSL 130
const glsl_version = 130
GLFW.WindowHint(GLFW.CONTEXT_VERSION_MAJOR, 3)
GLFW.WindowHint(GLFW.CONTEXT_VERSION_MINOR, 0)
# GLFW.WindowHint(GLFW.OPENGL_PROFILE, GLFW.OPENGL_CORE_PROFILE) # 3.2+ only
# GLFW.WindowHint(GLFW.OPENGL_FORWARD_COMPAT, GL_TRUE) # 3.0+ only
end
function julia_main()
try
real_main()
catch
Base.invokelatest(Base.display_error, Base.catch_stack())
return 1
end
return 0
end
function real_main()
# setup GLFW error callback
error_callback(err::GLFW.GLFWError) = @error "GLFW ERROR: code $(err.code) msg: $(err.description)"
GLFW.SetErrorCallback(error_callback)
# create window
window = GLFW.CreateWindow(1280, 720, "Demo")
@assert window != C_NULL
GLFW.MakeContextCurrent(window)
GLFW.SwapInterval(1) # enable vsync
# setup Dear ImGui context
ctx = CImGui.CreateContext()
# setup Dear ImGui style
CImGui.StyleColorsDark()
# CImGui.StyleColorsClassic()
# CImGui.StyleColorsLight()
# load Fonts
# - If no fonts are loaded, dear imgui will use the default font. You can also load multiple fonts and use `CImGui.PushFont/PopFont` to select them.
# - `CImGui.AddFontFromFileTTF` will return the `Ptr{ImFont}` so you can store it if you need to select the font among multiple.
# - If the file cannot be loaded, the function will return C_NULL. Please handle those errors in your application (e.g. use an assertion, or display an error and quit).
# - The fonts will be rasterized at a given size (w/ oversampling) and stored into a texture when calling `CImGui.Build()`/`GetTexDataAsXXXX()``, which `ImGui_ImplXXXX_NewFrame` below will call.
# - Read 'fonts/README.txt' for more instructions and details.
fonts_dir = joinpath(@__DIR__, "..", "fonts")
fonts = CImGui.GetIO().Fonts
# default_font = CImGui.AddFontDefault(fonts)
# CImGui.AddFontFromFileTTF(fonts, joinpath(fonts_dir, "Cousine-Regular.ttf"), 15)
# CImGui.AddFontFromFileTTF(fonts, joinpath(fonts_dir, "DroidSans.ttf"), 16)
# CImGui.AddFontFromFileTTF(fonts, joinpath(fonts_dir, "Karla-Regular.ttf"), 10)
# CImGui.AddFontFromFileTTF(fonts, joinpath(fonts_dir, "ProggyTiny.ttf"), 10)
CImGui.AddFontFromFileTTF(fonts, joinpath(fonts_dir, "Roboto-Medium.ttf"), 16)
# @assert default_font != C_NULL
# setup Platform/Renderer bindings
ImGui_ImplGlfw_InitForOpenGL(window, true)
ImGui_ImplOpenGL3_Init(glsl_version)
try
show_demo_window = true
show_another_window = false
clear_color = Cfloat[0.45, 0.55, 0.60, 1.00]
while !GLFW.WindowShouldClose(window)
GLFW.PollEvents()
# start the Dear ImGui frame
ImGui_ImplOpenGL3_NewFrame()
ImGui_ImplGlfw_NewFrame()
CImGui.NewFrame()
# show the big demo window
show_demo_window && @c CImGui.ShowDemoWindow(&show_demo_window)
# show a simple window that we create ourselves.
# we use a Begin/End pair to created a named window.
@cstatic f=Cfloat(0.0) counter=Cint(0) begin
CImGui.Begin("Hello, world!") # create a window called "Hello, world!" and append into it.
CImGui.Text("This is some useful text.") # display some text
@c CImGui.Checkbox("Demo Window", &show_demo_window) # edit bools storing our window open/close state
@c CImGui.Checkbox("Another Window", &show_another_window)
@c CImGui.SliderFloat("float", &f, 0, 1) # edit 1 float using a slider from 0 to 1
CImGui.ColorEdit3("clear color", clear_color) # edit 3 floats representing a color
CImGui.Button("Button") && (counter += 1)
CImGui.SameLine()
CImGui.Text("counter = $counter")
CImGui.Text(@sprintf("Application average %.3f ms/frame (%.1f FPS)", 1000 / CImGui.GetIO().Framerate, CImGui.GetIO().Framerate))
CImGui.End()
end
# show another simple window.
if show_another_window
@c CImGui.Begin("Another Window", &show_another_window) # pass a pointer to our bool variable (the window will have a closing button that will clear the bool when clicked)
CImGui.Text("Hello from another window!")
CImGui.Button("Close Me") && (show_another_window = false;)
CImGui.End()
end
# rendering
CImGui.Render()
GLFW.MakeContextCurrent(window)
display_w, display_h = GLFW.GetFramebufferSize(window)
glViewport(0, 0, display_w, display_h)
glClearColor(clear_color...)
glClear(GL_COLOR_BUFFER_BIT)
ImGui_ImplOpenGL3_RenderDrawData(CImGui.GetDrawData())
GLFW.MakeContextCurrent(window)
GLFW.SwapBuffers(window)
end
catch e
@error "Error in renderloop!" exception=e
Base.show_backtrace(stderr, catch_backtrace())
finally
ImGui_ImplOpenGL3_Shutdown()
ImGui_ImplGlfw_Shutdown()
CImGui.DestroyContext(ctx)
GLFW.DestroyWindow(window)
end
end
end # module
#GuiTest.real_main()
とりあえず動くことを確認するためには、#GuiTest.real_main() の#を外してから、VSCodeででも実行すればよいです。Shift+EnterでOKですね。動作確認完了後は、#を戻しておきましょう。
PackageCompiler.jlでappを作成する
以下のコマンドを実行すれば、appを作成することができます。結構時間はかかります。
using PackageCompiler
create_app("GuiTest", "GuiTestapp")
GuiTestappフォルダの中のbinというフォルダの中に、GuiTest.exeというファイルが生成されており、これを実行すれば良いです。
私のWindows10の環境では、生成されたコードは354MB程度になりました。(PackageCompiler.jlにしては)軽量です。
また、JuliaをインストールしていないWindowsにフォルダごとコピーして、binフォルダ内のGuiTest.exeを実行すると、多分動くと思います。
Plot環境を整える
せっかくアプリを作っても、PlotができないとJuliaでコードを書く楽しさが半減します。しかし、Plots.jlは現状いまいち正しくビルドができませんでした。Gadflyも、Makieも、ダメでした。そもそも、容量が増えるので結構つらい選択肢になります。
そこで、CImGui.jl上で使えるImPlot.jlを使いたいと思います。ImPlot.jlを使えば、おそらくC++ライブラリのwrapperのため、容量はそこまで膨らまないと考えられます。
しかし、2020/12/20現在では、ImPlot.jlを使うとPackageCompilerで動いてくれませんでした。どうやら、CImGuiが、v1.79.0の場合は、PackageCompilerに対応しているようですが、ImPlot.jlを利用するとCImGuiのバージョンがv1.77.1になり、これがダメなようです。
ImPlot.jlがバージョンアップすれば、うまくいくような気がするのですが・・・。
最後に
CImGui.jlとImPlot.jlを使えば、軽量且つかっこいいアプリを、Juliaで簡単に作ることが出来そうです。残念ながら、ImPlotは現状ではPackageCompiler.jlで動きませんが、ここがいい感じになれば、JuliaでかっこいいGUIアプリを作るのは、もう一般的になるのでは?という気もします。
Juliaすごい!