[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;
}

[C++] 328 BBS閲覧アプリの製作 その14 Fl_Tableの行番号取得 user_data メモリアドレス確認

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

Fl_Tableに設定したFl_Buttonに行番号を仕込み、クリック時に取り出せるようにしました。

初めはクリックしたボタンのラベル(スレッドタイトル)からスレッドIDを検索していたのですが、重複スレッドがある場合は検索結果がおかしくなるため、行番号でスレッドIDを検索するようにしました。

この実装がかなり難航しました。いよいよLLDBデバッガの出番かという手前で何とか解決させました。

行番号をメモリに格納させたまではよかったのですが、コールバック関数の方で正しいメモリアドレスを受け取れていませんでした。

検証方法を書き留めておきます。

void WidgetTable::SetSize(int newrows, int newcols) {
    <略>
    // 行番号rをuserDataとしてメモリに格納しFl_Buttonに紐付ける
    int* userData = new int(r);
    int rowPos = *static_cast<int*>(userData);
    cout << "rowPos: " << rowPos << " userData: " << userData << endl;

    Fl_Button *btn = new Fl_Button(X,Y,W,H);
    btn->user_data((void*)userData);
    btn->callback(button_cb);

// コールバック関数でメモリアドレスと行番号を確認
void button_cb(Fl_Widget *w, void* userData) {
    int rowPos = *static_cast<int*>(userData);
    cout << "userData: " << userData << endl;
    cout << "rowPos: " << rowPos << endl;
// userDataを正常に渡せた場合 : クリックした行番号9のメモリアドレスが一致している

rowPos: 0 userData: 0x6000015c1fa0
rowPos: 1 userData: 0x6000015c25f0
rowPos: 2 userData: 0x6000015c1fb0
rowPos: 3 userData: 0x6000015c2060
rowPos: 4 userData: 0x6000015c1fc0
rowPos: 5 userData: 0x6000015c2070
rowPos: 6 userData: 0x6000015c20c0
rowPos: 7 userData: 0x6000015c2160
rowPos: 8 userData: 0x6000015c2110
rowPos: 9 userData: 0x6000015c20d0 <-
rowPos: 10 userData: 0x6000015c21b0

// ボタンをクリック時(コールバック関数実行)
userData: 0x6000015c20d0 <- メモリアドレスが一致
rowPos: 9

[C++] 327 BBS閲覧アプリの製作 その13 右寄せ用Fl_Input継承クラス

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

スレッド番号とレス数をFl_Inputで左寄せ表示していたのですが、継承クラスで右寄せ用にしました。

narkiveという掲示板で17年前(2006年)にコードがアップされていました。クラスファイル化して使わせていただきます。ありがたいことです。

Fl_InputではGUI上のデータを編集できてしまうので、本当はFl_Boxで表示させたいのですが上手くできません。

あとレス表示欄も改行を入れるなどして見やすくしました。

次に手掛けるとしたら、スレッド一覧やスレッドのリロード、未読レスのカウントあたりでしょうか。

今のところ画像表示はできませんが、AIを使って内容の安全性を確認できたら面白いかもしれません。

それにしてもFLTKに関してはChatGPTは役に立たないどころか、あることないことでっち上げてマイナス面が大きいです。ネット情報、ユーザーの少なさの証左でもあり寂しくなります。

#include "InputRt.h"

InputRt::InputRt(int x, int y, int w, int h, const char *title = 0) : Fl_Input(x, y, w, h, title){};

InputRt::~InputRt(){}

void InputRt::draw(void){
	if (input_type() == FL_HIDDEN_INPUT) return;

	// Simplest to just redraw the whole box every time...
	Fl_Boxtype b = box();
	damage(FL_DAMAGE_ALL);
	draw_box(b, color());

	int xo = x()+Fl::box_dx(b);
	int yo = y()+Fl::box_dy(b);
	int wo = w()-Fl::box_dw(b);
	int ho = h()-Fl::box_dh(b);
	int wt, ht;
	char buf[128];

	// How long is the string to display?
	strncpy(buf, value(), 128);
	wt = 0; ht = 0;
	fl_measure(buf, wt, ht);

	// Make the text window be at the right hand end
	wt = wt + 5;
	xo = xo + wo - wt;
	wo = wt;

	// Update the text window
	Fl_Input_::drawtext(xo, yo, wo, ho);
}
#include <string.h>
#include <FL/Fl.H>
#include <FL/fl_draw.H>
#include <FL/Fl_Double_Window.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Input.H>

class InputRt : public Fl_Input{
	public:
		InputRt(int x, int y, int w, int h, const char *title);
		~InputRt();
		
	protected:
		void draw(void);

	private:
};

参考サイト