[C++] 338 BBS閲覧アプリの製作 その24 GUIに変数が表示されない場合 / DL済レス数表示および自動スクロール JavaScript

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

これまではnew/deleteやmalloc/freeで変数をメモリに格納して解放するまで保持できるようにしていましたが、パブリック変数にするだけで同様の効果が得られることに今さら気が付きました。

変数名が分かってしまうとデータ改変の可能性が高まりますが、自分用のアプリだったら問題ないでしょう。まあ、スコープをむやみに広げるのは良くないですが。

リロード時刻をGUIに表示できず、少々手間取っているうちに他ソースファイルでの使用不使用に係わらずパブリック変数にしてしまえばいいという手抜きに気付いたのでした。

追加機能ですが、DL済レス数を表示できるようにしました。次にDATファイルをダウンロードして表示する際は前回ダウンロードした最後のレスへ自動スクロールします。

<script>
var element = document.getElementById(305);
var rect = element.getBoundingClientRect();
var y = rect.top;
console.log("toOldRes y = " + y);
scrollTo(0, y);
</script>

[C++] 337 BBS閲覧アプリの製作 その23 ポップアップ位置の改善 JavaScript

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

レスアンカーのポップアップの位置をFl_WebViewの先頭やや下に固定していましたが、視線の動きを減らすため、ホバーした位置の近くに変更しました。

正確にはホバー位置を基準に100px上方へずらしています。上にはみ出さないよう調整しています。

ところでアプリのサイズは今のところ2.7MBになります。curlやxml2ライブラリは含みません。かなり軽量でかつ高速です。

単機能Electronアプリ(JavaScript)210MBやJavaアプリを除くと、これまで作ってきたデスクトップアプリでは最大サイズになりました。ちなみにSwiftで作ったメモアプリiOS版、ChatGPTアプリiOS版はそれぞれ4.2MB、2.7MBです。

function showPopup(id, event) {
    var element = document.getElementById(id);
    element.style.visibility = (element.style.visibility == 'visible')? "hidden": "visible";

    var rect = element.getBoundingClientRect();
    var y1 = rect.top;
    console.log("Popup y1 = " + y1);

    var y2 = event.clientY;
    console.log("Popup y2 = " + y2);

    var y = -y1 + y2 -100;
    if (y < -y1){
        y = -y1;
    }
    console.log("Popup y = " + y);

    element.style.top = y;
}
<p id="251" class="tips"><font size="2" color="#008080">251 : 投稿者名
<br></font><font size="2" color="#000080">2023/07/23(日) 19:36:50.61 ID:xxx<br></font>
<font size="2" color="#000B00"> <span onmouseover = "showPopup(230,event);" onmouseout = "showPopup(230,event);" >
<a href="../xxx/230" rel="noopener noreferrer" target="_blank">>>230</a></span>
<br> 本文 </font></p>

[C++] 336 BBS閲覧アプリの製作 その22 Fl_WebViewにコンソール代替機能実装 JavaScript

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

Fl_WebViewはGoogle ChromeのようなWebブラウザに近い機能を持っていますが、さすがに検証機能はありません。

そこでコンソールのような機能を持たせるため、テスト用のポップアップ要素をあらかじめ仕込んでおき、必要時にテスト関数でポップアップに結果を出力させるようにしました。

下図の例では、GUI右下のスクロールボタンをホバーした時にテスト関数が動作し、その結果を右上のポップアップに出力しています。

この機能により、Fl_WebViewは最新のJavaScriptであるES2022に対応していることが分かりました。

普段はChromeの検証機能を使えばいいと思いますが、Fl_WebView固有の性質を調べる場合は今回の機能を使用します。

function test(){
    const msg = "This browser supports ES2022.";
    const msg2 = "This browser don't supports ES2022.";

    var element = document.getElementById("test");
    element.style.visibility = (element.style.visibility == 'visible')? "hidden": "visible";
    var rect = element.getBoundingClientRect();
    var y = rect.top;
    console.log("Popup y = " + y);
    element.style.top = -y + 20;

    try {
        eval('class Foo { #bar = 42; }');
        element.textContent = msg;
    } catch (error) {
        element.textContent = msg2;
    }
}

[C++] 335 BBS閲覧アプリの製作 その21 リロード機能実装 

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

アプリにスレッド一覧とスレッドのリロード機能を実装しました。

これで製作前に使用していた既存BBSアプリに抱いていたUIへの不満点をほとんど解消した形になりました。黒地に赤字の見にくい配色、リロードボタンの不可解な位置、最上部・最下部へのスクロールボタンの小ささなどUI開発者の端くれとして看過できませんでした。過疎っているmacOSアプリ界隈ですから、それでも寡占状態になっています。

JavaScriptのポテンシャルを生かして画像表示などさらに機能を増やしていくつもりです。

[C++] 334 BBS閲覧アプリの製作 その20 スクロールボタン配置 JavaScript

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

HTML右端にスクロールボタンを配置し、最上部、最下部へ移動できるようにしました。ボタンはスクロールしても動きません。

Fl_Buttonで移動させたかったのですが、Fl_WebViewの子要素としてスクロールバーを把握する方法が分からないため、この手法にしました。

ここ数日結構根を詰めて取り組んだので、しばらくのんびりします。

振り返ればプログラミングを始めて3年半が経ちました。進度としてはまずまずだと思います。

    <html lang="ja">
    <HEAD>
    <meta charset="utf-8">
    <STYLE TYPE="text/css">
    A:link {    color: #0000ff;
                text-decoration: none; }
    A:visited { color: #008080;
                text-decoration: none; }
    A:hover {   color: #ff0000;
                text-decoration: underline; }
    body {
        font-family:"Helvetica","ヒラギノ角ゴ";
        margin-left: 20px;
    }

    .tips {
        position: absolute;
        top: 0px;
        left: 100px;
        visibility:hidden;
        background-color: #E0FFFF;
        margin-left:2%;
        padding:1em;
    }

    .rightTop-fixed-button {
        position: fixed;
        top: 0;
        right: 0;
        width: 50px;
        height: 20px;
        /* padding-top: 0px; */
        text-align: center;
        background: #FFFFFF;
        border-top: 4px solid #fff;
    }

    .rightTop-fixed-button button {
        font-size: 16px;
        cursor: pointer;
        vertical-align: middle;
    }

    .rightTop-fixed-button button:hover {
        opacity: 0.2;
    }

    .rightBtm-fixed-button {
        position: fixed;
        bottom: 10px;
        right: 0;
        width: 50px;
        height: 20px;
        /* padding-top: 0px; */
        text-align: center;
        background: #FFFFFF;
        border-top: 4px solid #fff;
    }

    .rightBtm-fixed-button button {
        font-size: 16px;
        cursor: pointer;
        vertical-align: middle;
    }

    .rightBtm-fixed-button button:hover {
        opacity: 0.2;
    }

    </STYLE>
    <script>
        function showPopup(id) {
            if(document.getElementById){
                var element = document.getElementById(id);
                element.style.visibility = (element.style.visibility == 'visible')? "hidden": "visible";

                var rect = element.getBoundingClientRect();
                var y = rect.top;
                console.log("Popup y = " + y);

                element.style.top = -y + 20;
            }
        }

        function moveTop(){
            scrollTo(0,0);
        }

        function moveBottom(){
            let scrollHeight = Math.max(
            document.body.scrollHeight, document.documentElement.scrollHeight,
            document.body.offsetHeight, document.documentElement.offsetHeight,
            document.body.clientHeight)

            scrollTo(0, scrollHeight);
        }
    </script>
    
    
    </HEAD>
    <BODY bgcolor="#ffffff">

    <div class="rightTop-fixed-button">
    <button onclick="moveTop();">|<</button>
    </div>
    <div class="rightBtm-fixed-button">
    <button onclick="moveBottom();">>|</button>
    </div>

<以下略>

[C++] 333 BBS閲覧アプリの製作 その19 レスアンカー先をポップアップ表示 Y座標の取得 JavaScript

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

前回の設定ではポップアップのTOP位置が絶対座標のゼロだったため、スクロールすると見えなくなってしまいます。

ホバーした時のFl_WebView上端Y座標を基準にTOP設定することで常にGUI上にポップアップが出現するようにしました。

Google Chromeの検証画面でY座標を出力させながら動作確認しました。生成するHTMLはサイズが倍になりましたが、表示速度は体感では変わらずでした。

今回はJavaScript初学者にはキツい内容でした。4時間ほど掛かったでしょうか。ChatGPTはまあまあ貢献してくれました。

Chrome検証画面で動作確認
<html lang="ja">
    <HEAD>
    <meta charset="utf-8">
    <STYLE TYPE="text/css">
    A:link {    color: #0000ff;
                text-decoration: none; }
    A:visited { color: #008080;
                text-decoration: none; }
    A:hover {   color: #ff0000;
                text-decoration: underline; }
    body {
        font-family:"Helvetica","ヒラギノ角ゴ";
        margin-left: 20px;
    }

    .tips {
        position: absolute;
        top: 0px;
        left: 100px;
        visibility:hidden;
        background-color: #E0FFFF;
        margin-left:2%;
        padding:1em;
    }

    </STYLE>
    <script>
        function showPopup(id) {
            if(document.getElementById){
                var element = document.getElementById(id);
                element.style.visibility = (element.style.visibility == 'visible')? "hidden": "visible";

                var rect = element.getBoundingClientRect(); // ホバー時のFl_WebViewを取得する
                var y = rect.top; // 上端のY座標を取得する
                console.log("Popup y = " + y); // Y座標を出力

                element.style.top = -y + 20; // Y座標に-1を掛け20px下へずらした位置をTOPとする          
            }
        }
    </script>
<以下略>
// GUI表示用
<p><font size="2" color="#008080">5 : 投稿者名 <br>
</font><font size="2" color="#000080">2023/07/22(土) 18:48:05.94 ID:xxx<br>
</font><font size="2" color="#000B00"> <span onmouseover = "showPopup(2);" onmouseout = "showPopup(2);">
<a href="../test/read.cgi/xxx/2" rel="noopener noreferrer" target="_blank">>>2</a>
</span> <br> 本文 </font></p>

// ポップアップ用
<p id="5" class="tips"><font size="2" color="#008080">5 : 投稿者名 <br>
</font><font size="2" color="#000080">2023/07/22(土) 18:48:05.94 ID:xxx<br>
</font><font size="2" color="#000B00"> <span onmouseover = "showPopup(2);" onmouseout = "showPopup(2);">
<a href="../test/read.cgi/xxx/2" rel="noopener noreferrer" target="_blank">>>2</a>
</span> <br> 本文 </font></p>

[C++] 332 BBS閲覧アプリの製作 その18 レスアンカー先をポップアップ表示 JavaScript

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

Fl_WebViewではSTYLEタグやJavaScriptを使えます。

レスアンカーをホバーするとアンカー先の内容をポップアップするようにしました。

現時点ではFl_WebView表示用とポップアップ用を作成する必要があるので単純にHTMLの全体量は倍になります。

まだ検証していないのですが、表示速度に大きな影響があれば改良を検討します。

<html lang="ja">
    <HEAD>
    <meta charset="utf-8">
    <STYLE TYPE="text/css">
    A:link {    color: #0000ff;
                text-decoration: none; }
    A:visited { color: #008080;
                text-decoration: none; }
    A:hover {   color: #ff0000;
                text-decoration: underline; }
    body {
        font-family:"Helvetica","ヒラギノ角ゴ";
        margin-left: 20px;
    }

    .tips {
        position: absolute;
        top: 20px;
        left: 100px;
        visibility:hidden;
        background-color: #E0FFFF;
        margin-left:2%;
        padding:1em;
    }

    </STYLE>
    <script>
        function showPopup(id) {
            if(document.getElementById){
                var element = document.getElementById(id);
                element.style.visibility = (element.style.visibility == 'visible')? "hidden": "visible";
            }
        }
    </script>
<以下略>

[C++] 331 BBS閲覧アプリの製作 その17 Fl_WebViewの導入方法

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

昨日の段階でFl_WebViewの埋め込みに成功したかに見えましたが、後でGUI左側のボード一覧、右上のスレッドタイトル一覧が全く動かなくなっていることが判明しました。

その問題については先ほど解決しましたので、合わせて導入手順を以下にまとめます。

1.FLTK 1.4.0 開発版をダウンロードし、/usr/localディレクトリへインストールする。
これまで1.4.0はApple Siliconでビルドできなかったのですが、20230714版はできました。正式リリースが確実に近づいているようです。

2.GitHubからFl_WebViewをダウンロードする。

3.解凍して/src/Fl_WebView.cxxの以下の行をコメントアウトする(私のアプリの場合)。

void Fl_WebView::init() {
  if (!shown())
    throw std::runtime_error("The window needs to be shown.");
  auto handle = fl_xid(this);
  wv = webview_create(false, (void *)handle);
  make_delegate((void *)webview_get_window(wv), (void *)handle);
  // Fl::add_idle(webview_run, wv); // この行をコメントアウトする
  this->top_window()->callback(close_cb, (void *)webview_get_window(wv));
}

4.README.mdに従ってビルドする。FLTK 1.4.0存在下でビルドしないと後々不具合が生じる(1.3.8でビルドしたウィジェットでFl_Groupの関数が存在しないことに起因するビルドエラー発生事例あり)。

5./bin/libfltk_webview.aと/include/Fl_WebView.Hにより、Fl_WebViewウィジェットが使えるようになる。

Fl_WebViewの作者はこのウィジェットをGUI内で他ウィジェットと共存させるケースを想定していなかったようです。Google Chromeなどのようにウィンドウ一面を占める前提になっています。

私のアプリではGUI左側、右上も引き続き使用するので、Fl_WebView::init()にあるウィジェットへのhandle移動に関わる部分を無効にしました。

Makefileは以下のようになります。

# コンパイラ
COMPILER = clang++
DEBUG = -g

# オプション
CPPFLAGS = -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_THREAD_SAFE -D_REENTRANT -std=c++20
LDFLAGS = -lfltk -lpthread -framework Cocoa -framework WebKit -lc++

# includeパス(-I)
INCLUDE = -I./include -I/Volumes/DATA_m1/code/cpp/mylib/include -I/usr/local/include \
-I/opt/homebrew/Cellar/libxml2/2.10.3_2/include \
-I/library/Fl_WebView/include

# ライブラリ(-l)
LIBRARY0 = -lcurl -liconv -lxml2 \
/library/Fl_WebView/bin/libfltk_webview.a \

# ライブラリパス(-L)
LIBRARY = -L/usr/local/lib

# ソースファイル
SRCDIR = ./src
SRCS = $(shell find $(SRCDIR) -type f -not -name ".DS_Store")

# オブジェクトファイル
OBJDIR = ./obj
OBJS = $(addprefix $(OBJDIR), $(patsubst ./src/%.cpp,/%.o,$(SRCS)))

# 実行ファイル
TARGETDIR = ./bin
TARGET = BBS_Browser

# cppファイルからoファイル作成 $<:依存ファイル
$(OBJDIR)/%.o: $(SRCDIR)/%.cpp
	$(COMPILER) $(CPPFLAGS) $(INCLUDE) $(DEBUG) -o $@ -c $<

# アプリファイル作成関連
POSTBUILD  = fltk-config --post

# oファイルから実行ファイルとappファイル作成
$(TARGET):$(OBJS)
	$(COMPILER) -o $(TARGETDIR)/$@ $(OBJS) $(LIBRARY0) $(LDFLAGS) $(LIBRARY) 
	cp $(TARGETDIR)/$(TARGET) $(TARGET)
	$(POSTBUILD) $(TARGET)
	mkdir $(TARGET).app/Contents/Resources
	cp ./images/$(TARGET).icns $(TARGET).app/Contents/Resources
	plutil -insert 'CFBundleIconFile' -string $(TARGET).icns $(TARGET).app/Contents/Info.plist
	rm -f $(TARGET)

# 全ソース強制コンパイル
.PHONY:all
all: clean $(TARGET)

# 全ファイル削除
.PHONY:clean
clean:
	rm -rf $(OBJS) $(TARGETDIR)/$(TARGET) $(TARGET).app

[C++] 330 BBS閲覧アプリの製作 その16 Fl_WebViewでHTML表示に成功

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

FLTKのRust版を開発されている方がFl_WebViewというウィジェットをGitHubにアップしていました。

GUIの中にWebブラウザの埋め込みが可能になります。BBSのdatファイルをHTML形式に書き換えたものを表示させることもできました。右端での折り返しはデフォルトです。CSSへの対応についてはこれから確認します。

ただ最新版のFLTK 1.3.8では上手くできません。1.4.0の開発版が必要になります。

詳しくは次回以降の記事でまとめます。

#include "Fl_WebView.H"
<中略>

int main(int argc, char *argv[]) {
    Fl_Double_Window *win = new Fl_Double_Window(1400, 1020, "BBS Browser");
    win->position(300, 0);
    win->resizable(win);

    Fl_Tree* tree = new Fl_Tree(10, 45, 230, win->h()-45-10);
    tree->showroot(0);
    tree->callback(treeCallback); 

    urlNameCat = getBoard();

    for (const auto& element : urlNameCat) {
        string path = get<2>(element) + "/" + get<1>(element);
        // cout << "path: " << path << endl;

        string category = get<2>(element);

        if (find(categories.begin(), categories.end(), category) == categories.end()) {
            tree->add(category.c_str())->close();
            categories.push_back(category);
        }

        tree->add(path.c_str());
    }

    table = new WidgetTable(240, 45, 1400-230-10, 350, "");

    htmlView = new Fl_WebView(240,400,1160,615);
    
    win->show(argc, argv);
  
  return(Fl::run());
}

参考サイト

[C++] 329 BBS閲覧アプリの製作 その15 Fl_Help_Viewでレス表示

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

これまでスレッドのレスはFl_Text_Displayで表示していましたが、フォントのサイズや色設定をしたいのでFl_Help_Viewに変更しました。

datファイルをHTML形式に変換しています。

ただしFl_Help_ViewはHTMLの簡易的なビューアのため、CSSは使えず右端の折り返しも不可です。日本語フォントの設定も不可で、強制的に明朝体になります。FLTKの限界と言ったところでしょうか。wxWidgetsでもCSSはほとんど使えないようです。

C++での開発はこれで打ち切りとし、SwiftあるいはObjective-C++へ移植の予定です。

Objective-C++では過去に1つだけ簡単なGUIアプリを作りましたが、AppDelegateファイルでしんどい思いをしたので、このファイルを使わずに済ませる方法を探します。もちろん非Xcode環境で開発します。

void button_cb(Fl_Widget *w, void* userData) {
    int postNum;

    int rowPos = *static_cast<int*>(userData);
    postNum = stoi(std::get<3>(numTitlePostnumID[rowPos]));
    cout << "postNum: " << to_string(postNum) << endl;

<中略>

    string html;
    string line;

    string header = R"(
    <html lang="ja"><HEAD>
    <link rel="stylesheet" href="currentThread.css">
    <STYLE TYPE="text/css">
    A:link {    color: #0000ff;
                text-decoration: none; }
    A:visited { color: #008080;
                text-decoration: none; }
    A:hover {   color: #ff0000;
                text-decoration: underline; }
    body {
        margin-left: 20px;
    }

    </STYLE>
    </HEAD>
    <BODY bgcolor="#ffffff"><font face="ヒラギノ角ゴ">)";

    std::ifstream datFile0(filename);
    if (!datFile.is_open()) {
        cout << "ファイルopen失敗" << endl;

        return;
    }

    int resNum = 1;    
    while (std::getline(datFile, line)) {
        string sage;

        // mailNameを抽出
        size_t pos = line.find("<>sage<>", 0);
        if (pos != std::string::npos) {
            sage += " sage ";
        }

        size_t posAngle1 = line.find("<>");
        string mailName = line.substr(0, posAngle1);
        mailName += sage;

        cout << "resNum: " << resNum << endl;
        cout << "mailName: " << mailName << endl;

        // 日付を抽出
        size_t posAngle2 = line.find("<>", posAngle1 + 2);
        size_t posAngle3 = line.find("<>", posAngle2 + 2);

        string date = line.substr(posAngle2 + 2, posAngle3 - posAngle2 - 2);

        cout << "date: " << date << endl;

        // メッセージを抽出
        size_t posAngle4 = line.find("<>", posAngle3 + 2);
        string msg = line.substr(posAngle3 + 2, posAngle4 - posAngle3 - 2);

        cout << "msg1: " << msg << endl;

        string res;
        if (resNum == 1){
            res = header + "<p><font size=\"4\" color=\"#008080\">" + to_string(resNum) + " : " + mailName + "<br></font>" +
            "<font size=\"4\" color=\"#000080\">" + date + "<br></font>" +
            "<p> </p>" +
            "<font size=\"4\" color=\"#000B00\">" + msg  + "</font></p>";
        } else if (resNum != postNum){
            res = "<p><font size=\"4\" color=\"#008080\">" + to_string(resNum) + " : " + mailName + "<br></font>" +
            "<font size=\"4\" color=\"#000080\">" + date + "<br></font>" +
            "<p> </p>" +
            "<font size=\"4\" color=\"#000B00\">" + msg  + "</font></p>";
        } else{
            res = "<p><font size=\"4\" color=\"#008080\">" + to_string(resNum) + " : " + mailName + "<br></font>" +
            "<font size=\"4\" color=\"#000080\">" + date + "<br></font>" +
            "<p> </p>" +
            "<font size=\"4\" color=\"#000B00\">" + msg  + "</font></p></font></BODY></html>";
        }

        cout << "res:\n" << res << endl;

        html += res;

        resNum += 1;
    }

    cout << "html:\n" << html << endl;

    // htmlファイル保存
    std::ofstream outputFile("/BBS_Browser/html/currentThread.html");
    if (outputFile.is_open()) {
        outputFile << html;
        outputFile.close();
        std::cout << "HTMLファイルに変換しました。" << std::endl;
    } else {
        std::cerr << "ファイルを開けませんでした。" << std::endl;
        return;
    }

    // // htmlファイルを表示
    htmlView->load("/BBS_Browser/html/currentThread.html");

    return;
}