ascolorbox の practicebox におけるタイトル部分の汎用化 with Lua言語(LaTeX 確認シリーズ #3)
practicebox
今回は、鉄緑会の「練習問題」を入れるボックスでお馴染みの practicebox(ascolorbox に含まれるボックスの1つ)を魔改造したのでご紹介します。ただし、Lua言語を使って書いていますので、LuaLaTeX以外ではエラーを吐きます。ご注意ください。
このボックスの構造は、ある程度以上の上級者でなければ理解できないと思います。と言いますのも、
-
tikzの基本構文 -
tcolorboxについての基本知識 - ループ構文への理解
がないとまともに読めないからです。
さらに、今回は Lua 言語にも頼っていますので、ガチで読もうとすると骨が折れることが予想されます。
よって、自信がない方はコードをコピペするだけでも問題ないと思います。
追記2025/09/11
引数の順序の改善、およびタイトル文字数の自動取得に関する改善を行いました。
やりたいこと
そもそも、practiceboxというのは
こういうやつです。しかし、オリジナルのコードでは、
\DeclareTColorBox{practicebox}{ m O{} }%
{%省略
boxed title style={empty,arc=0pt,outer arc=0pt,boxrule=0pt},
underlay unbroken and first={
\draw[black,ultra thick] (frame.north west) -- (frame.north east);
\draw[black] ([yshift=-.8mm]frame.north west) -- ([yshift=-.8mm]frame.north east);
\fill[black] ([yshift=1mm]frame.north west) rectangle +(6mm,6mm);
\node[white] at ([xshift=3mm,yshift=4mm]frame.north west){\large\ascb@textgt{練}};
\node at ([xshift=9mm,yshift=4mm]frame.north west){\large\ascb@textgt{習}};
\fill[black] ([xshift=12mm,yshift=1mm]frame.north west) rectangle +(6mm,6mm);
\node[white] at ([xshift=15mm,yshift=4mm]frame.north west){\large\ascb@textgt{問}};
\node at ([xshift=21mm,yshift=4mm]frame.north west){\large\ascb@textgt{題}};
%省略
},
#2}
となっており、タイトルである「練習問題」というのが固定なのです。
当然用途を限定して使うのであれば良いのですが、自分はマクロにはある程度汎用性がないと困るという主義なので、
任意のタイトル(ただし長すぎないものとする)を格納でき、尚且つデザインはオリジナルのそれに忠実なものを作成する
という目標のもと、やっていきましょう。
ソースコード
長々解説を書く前に、さっさとソースコードをお見せします。
%汎用性アップバージョン
% 下準備(Lua言語を使用)
\newcommand{\getcharfrom}[2]{%
\begingroup
% 展開して値だけを取り出す(ローカル)
\def\GFstr{#1}%
\def\GFidx{#2}%
% Lua 側で UTF-8 の i 番目文字を取り出す
\directlua{%
local s = "\luaescapestring{\GFstr}"
local idx = tonumber("\luaescapestring{\GFidx}") or 0
local ch = unicode.utf8.sub(s, idx, idx) or ""
tex.sprint(ch)
}%
\endgroup
}
\newcounter{NN} % カウンタ NN を定義
\DeclareRobustCommand{\getstrlenNN}[1]{%
\begingroup
\edef\GLstr{#1}%
\directlua{%
local ss = "\luaescapestring{\GLstr}"
local lenn = unicode.utf8.len(ss) or 0
tex.sprint("\\setcounter{NN}{"..lenn.."}")
}%
\endgroup
}
% 長さのパラメータを用意
\newdimen\PracticeBoxUnit
\PracticeBoxUnit=6mm
\DeclareTColorBox{practicebox}{ m O{} O{}}%
{enhanced,sharp corners,colback=black!10!white, colframe=black!10!white , attach boxed title to top left={xshift=0mm,yshift=1mm}, fonttitle=\ascb@gtfamily,varwidth boxed title=0.85\linewidth, breakable, arc=0mm,title={#1},before={\getstrlenNN{#1}},
enlarge top by=2mm,
boxed title style={empty,arc=0pt,outer arc=0pt,boxrule=0pt},
underlay unbroken and first={
\draw[black,ultra thick] (frame.north west) -- (frame.north east);
\draw[black] ([yshift=-.8mm]frame.north west) -- ([yshift=-.8mm]frame.north east);
\pgfmathtruncatemacro{\N}{\arabic{NN}}%
% 1 から N まで繰り返す。
\foreach \i [count=\xi from 0] in {1,...,\N}{%
% \i の偶奇を計算(0=偶数,1=奇数)
\pgfmathtruncatemacro{\isodd}{mod(\i,2)}%
% 横シフト量を長さとして作る(\xlen に mm 等の長さが入る)
\pgfmathsetlengthmacro{\xlen}{\xi*\PracticeBoxUnit}%
\begin{scope}
% y方向だけ微調整
\ifnum\isodd=1\relax
% 奇数番目のとき
\filldraw[fill=black, draw=black, thick] ([yshift=1mm, xshift=\xlen]frame.north west) rectangle +(6mm,6mm); \node at ([xshift=3mm + \xlen,yshift=4mm]frame.north west){\large\textcolor{white}{\ascb@textgt{\getcharfrom{#1}{\i}}}};
\else
% 偶数番目のとき
\draw[black, thick] ([yshift=1mm, xshift=\xlen]frame.north west) rectangle +(6mm,6mm); \node at ([xshift=3mm + \xlen,yshift=4mm]frame.north west){\large\ascb@textgt{\getcharfrom{#1}{\i}}};
\fi
\end{scope}%
}%
\node[anchor=west] at ([xshift=3mm + \N*\PracticeBoxUnit,yshift=4mm]frame.north west) {\ascb@textgt{#2}};
},
underlay unbroken and last={
\draw[black,ultra thick] (frame.south west) -- (frame.south east);
\draw[black] ([yshift=.8mm]frame.south west) -- ([yshift=.8mm]frame.south east);
},
#3}
今回のコードは少々長いですね。
ちなみに、このコードは https://github.com/KKTeX/Publicated-Files/blob/main/textboxes_pub/PracticeBox_mydecseries.sty ここからも入手可能です。
使い方は
% 構文
\begin{practicebox}{タイトル}[サブタイトル][追加のキー(あまり使わない)]
本文
\end{practicebox}
% 実用
\begin{practicebox}{長めのタイトル}[\fbox{サブタイトル}]
あいうえお
\end{practicebox}
であり、出力は
となります。
構造解説
下準備
今回のゴールを達成する上での障壁は、
タイトルの文字数を取得しない限り、タイトルを囲うマス目の描画数が決まらない
というものになります。よって、これをどう取得するかが第1のボトルネックとなるわけですが、そこで使うのがLua言語です。(おそらくexpl3的なアプローチでもいけるのだとは思うのですが、上手くいかなかったため今回は不採用です。)
そういう訳なので、まずは
入力された文字列のうち、n 番目のものを取り出して出力する
ことのできるコマンドを作成します。
% 構文
\newcommand{\getcharfrom}[2]{%
\begingroup
% 展開して値だけを取り出す(ローカル)
\def\GFstr{#1}%
\def\GFidx{#2}%
% Lua 側で UTF-8 の i 番目文字を取り出す
\directlua{%
local s = "\luaescapestring{\GFstr}"
local idx = tonumber("\luaescapestring{\GFidx}") or 0
local ch = unicode.utf8.sub(s, idx, idx) or ""
tex.sprint(ch)
}%
\endgroup
}
% 実用
\getcharfrom{あいうえお}{4} % これにより「え」が出力される。
順を追って説明します。
\begin/endgroup でマクロをローカライズしたのち、\GFsrt に文字列を、\GFidx に取り出したい文字数が何番目にあるかを格納します。
そして、s に \GFsrt を移し替えます。また、\GFidx の値を idx に代入しておきます(それぞれ8、9行目)。or 以下は気にしなくて大丈夫です。
さらに unicode.utf8.sub(s, idx, idx) により、s における idx 番目から idx 番目(今回は1つしか取り出さないため、左端と右端がたまたま同じ。)の文字(UTF-8)を取り出します(10行目)。or 以下はこちらも気にしなくて大丈夫です。
最後に tex.sprint(ch) を使って、TeX に取得した ch を戻します(11行目)。
これで、文字列の取得は可能になりました。
マス目数取得の自動化
前項と同様にして、こちらもLua言語で対応します。
\newcounter{NN}
\DeclareRobustCommand{\getstrlenNN}[1]{%
\begingroup
\edef\GLstr{#1}%
\directlua{%
local ss = "\luaescapestring{\GLstr}"
local lenn = unicode.utf8.len(ss) or 0
tex.sprint("\\setcounter{NN}{"..lenn.."}")
}%
\endgroup
}
これにより、NNという名前のカウンタにマス目を取得するコマンドができました。
マス目数=\arabic{NN}
のように書くと、値を可視化できます。書き方は前項とほぼ同じなので割愛します。
パラメータの取得
このボックスのタイトル部分は、一辺の長さが 6mm の正方形でできていますが、"6mm" みたいな後々よく使う値は \PracticeBoxUnit のようにトークンにしておくのがコツです。書き方の説明は蛇足でしょう。
ボックス本体の改造
オリジナルの使える部分を残しつつ、必要な部分を変えていきます。
\DeclareTColorBox{practicebox}{ m O{} O{}}%
{enhanced,sharp corners,colback=black!10!white, colframe=black!10!white , attach boxed title to top left={xshift=0mm,yshift=1mm}, fonttitle=\ascb@gtfamily,varwidth boxed title=0.85\linewidth, breakable, arc=0mm,title={#1},before={\getstrlenNN{#1}},
enlarge top by=2mm,
boxed title style={empty,arc=0pt,outer arc=0pt,boxrule=0pt},
underlay unbroken and first={
\draw[black,ultra thick] (frame.north west) -- (frame.north east);
\draw[black] ([yshift=-.8mm]frame.north west) -- ([yshift=-.8mm]frame.north east);
\pgfmathtruncatemacro{\N}{\arabic{NN}}%
% 1 から N まで繰り返す。
\foreach \i [count=\xi from 0] in {1,...,\N}{%
% \i の偶奇を計算(0=偶数,1=奇数)
\pgfmathtruncatemacro{\isodd}{mod(\i,2)}%
% 横シフト量を長さとして作る(\xlen に mm 等の長さが入る)
\pgfmathsetlengthmacro{\xlen}{\xi*\PracticeBoxUnit}%
\begin{scope}
% y方向だけ微調整
\ifnum\isodd=1\relax
% 奇数番目のとき
\filldraw[fill=black, draw=black, thick] ([yshift=1mm, xshift=\xlen]frame.north west) rectangle +(6mm,6mm); \node at ([xshift=3mm + \xlen,yshift=4mm]frame.north west){\large\textcolor{white}{\ascb@textgt{\getcharfrom{#1}{\i}}}};
\else
% 偶数番目のとき
\draw[black, thick] ([yshift=1mm, xshift=\xlen]frame.north west) rectangle +(6mm,6mm); \node at ([xshift=3mm + \xlen,yshift=4mm]frame.north west){\large\ascb@textgt{\getcharfrom{#1}{\i}}};
\fi
\end{scope}%
}%
\node[anchor=west] at ([xshift=3mm + \N*\PracticeBoxUnit,yshift=4mm]frame.north west) {\ascb@textgt{#2}};
},
underlay unbroken and last={
\draw[black,ultra thick] (frame.south west) -- (frame.south east);
\draw[black] ([yshift=.8mm]frame.south west) -- ([yshift=.8mm]frame.south east);
},
#3}
まず1行目の引数仕様を変えます。
- 第1引数:タイトル
- 第2引数:サブタイトル
- 第3引数:追加のキー
1〜10行目まではオリジナルそのままで、11行以降がこの実装における肝となる、ループ処理となります。
ひとまず、タイトルの文字数を \N に入れてしまいましょう(12行目)。before で \getstrlenNN{#1} を回して文字数を取得し、それを \N に格納しています。そして、\foreach の構文に従い、\i というローカル変数をとり(シグマにおける k のような変数のことです。)、以下のように \i を2で割った剰余を以て場合分けします。また、i の増加に従って、マス目の描画位置は右にずれていきますから、そのずれ分も計算し、\xlen に入れておきます。
% \i の偶奇を計算(0=偶数,1=奇数)
\pgfmathtruncatemacro{\isodd}{mod(\i,2)}%
% 横シフト量を長さとして作る(\xlen に mm 等の長さが入る)
\pgfmathsetlengthmacro{\xlen}{\xi*\PracticeBoxUnit}%
そして、奇数番目では黒背景白字、偶数番目では白背景黒字ですので、
% y方向だけ微調整
\ifnum\isodd=1\relax
% 奇数番目のとき
\filldraw[fill=black, draw=black, thick] ([yshift=1mm, xshift=\xlen]frame.north west) rectangle +(6mm,6mm); \node at ([xshift=3mm + \xlen,yshift=4mm]frame.north west){\large\textcolor{white}{\ascb@textgt{\getcharfrom{#1}{\i}}}};
\else
% 偶数番目のとき
\draw[black, thick] ([yshift=1mm, xshift=\xlen]frame.north west) rectangle +(6mm,6mm); \node at ([xshift=3mm + \xlen,yshift=4mm]frame.north west){\large\ascb@textgt{\getcharfrom{#1}{\i}}};
\fi
のように描画を分けます。
ここまででループ処理完了です。
あとはサブタイトルですが、これは \N*\PracticeBoxUnit だけ描画位置を右へずらすだけですので問題ないでしょう。
それから、申し訳程度に第3引数として新たなキーを追加できるようになっていますが、正直これは使わなくて良いでしょう。オリジナルの引数の名残りです。
以上でボックスを作り終えました。
仕様上の注意
冒頭でも述べましたが、大前提として、タイトル部分に長いものを入れる前提で組んではおりません。せいぜい10文字と見ておいてください。それから、言うまでもありませんが、タイトルはUTF-8の文字限定です。数字・カナ・漢字・英字などは大丈夫ですが、TeX のマクロなんて入れようものなら即エラーです。ご注意ください。
終わりに
今回は、個人的に ascolorbox の中で最も汎用的でないと思っていたボックスを真改造してみました。正直まだ改善できるところもあるとは思いますが(文字数の自動取得のところなど)、それでもかなりの使い勝手の良さになったと思います。この記事が少しでも読者様のお役に立つことを願っております。

