MQL5取引ツール(第14回):アンチエイリアシングと角丸スクロールバーを備えたピクセルパーフェクトなスクロール対応テキストキャンバス
はじめに
前回の記事(第13回)では、MetaQuotes Language 5 (MQL5)のCCanvasクラスを使用して、価格グラフを可視化するCanvasベースの価格ダッシュボードを作成しました。このダッシュボードには、ラインプロットやフォグ効果を用いた価格表示機能に加え、口座情報やバー情報を表示する統計パネルを実装しました。また、背景画像やグラデーション、テーマ切り替え、ドラッグ移動、サイズ変更機能などをイベント処理とともにサポートしました。第14回では、MQL5の制限を回避するために、アンチエイリアス処理を施したピクセルパーフェクトなスクロール可能テキストキャンバスを追加し、さらに改良を加えています。この機能強化では、使用ガイド用のテキストパネルを導入し、カスタムのアンチエイリアス処理により、滑らかなエッジ表現を実現しています。ホバー時に拡張されるスクロールバーを備えており、ボタンやスライダーによる操作、ホイール入力への対応、テーマ付きの背景や透明度設定、動的な折り返し表示や色付きテキスト表示を含み、包括的なユーザー向け説明をシームレスに統合しています。本記事では以下のトピックを扱います。
最終的には、詳細でインタラクティブなテキストガイドパネルを備えたMQL5キャンバスダッシュボードが完成し、さらなるカスタマイズにも対応できるようになります。それでは、さっそく始めましょう。
ピクセルパーフェクトなスクロール可能テキストキャンバスフレームワークの理解
ピクセルパーフェクトなスクロール可能テキストキャンバスフレームワークは、MQL5のネイティブなテキストスクロールの制約を解決するために設計されています。この仕組みでは、滑らかなエッジを実現するアンチエイリアス付きのピクセルレベル描画を採用し、さらにホバー時に拡張される丸みのあるスクロールバーを用いることで、操作性を向上させています。また、上下ボタンやドラッグ可能なスライダーといったインタラクティブ要素を備え、長いコンテンツや使用ガイドの閲覧を直感的におこなえるようになっています。さらに、テーマ対応の背景と透明度調整機能をサポートし、パネル幅に応じた動的な行の折り返しを実現しながら、見出しやリンクの色情報を保持する設計となっています。マウスホイールによるスクロールにも対応しており、チャートのズーム操作と干渉しない形でテキスト領域内のスクロール制御を可能にしています。これにより、組み込みオブジェクトに依存せず、より精密な操作が実現されています。ダッシュボードとの統合によってイベントに応じたリアルタイム更新も可能となり、グラフ、統計、テキストパネル間で一体化されたモニタリングツールとして機能します。
本設計では、静的なMQL5オブジェクトによるライン描画は採用せず、キャンバスの機能を最大限活用する方針を取っています。これにより、過去の記事で発生していたようなテキストが領域外にはみ出す問題を回避でき、キャンバス側で自動的にクリッピング処理がおこなわれるため、Webページのようなスクロール体験を実現できます。また、丸みのあるダイナミックスクロールバーは、MetaQuotesの最新アップデートに見られるターミナルUIの洗練されたオーバーレイデザインから着想を得ています。以下は、スクロールバー設計の参考としたMetaQuotesのUIです。

今後の拡張として、テキストパネルの高さや透明度などの入力と列挙オプションの追加、テキストキャンバスオブジェクトの生成と破棄ロジックの実装、スクロール状態、位置、寸法、色に関するグローバル変数の定義、色検出や見出しの強調表示を伴うテキスト折り返し機能の実装を予定しています。さらに、円弧、円、矩形を用いたホバー効果付きの角丸スクロールバー(ボタンとスライダー含む)の描画、また、テキスト領域内でのホバー、クリック、ドラッグ、ホイール操作を処理し、スクロール位置やツールチップ表示の更新、必要に応じたスクロール無効化にも対応します。加えて、テーマ切り替えや最小化、サイズ変更操作に応じてテキストパネルが動的に調整される仕組みも組み込みます。以下に想定されるビジュアル表示の例を示します。

MQL5での実装
MQL5でプログラムを拡張するには、新しいテキストCanvasパネルを宣言し、レンダリングを動的に制御するための追加の入力とグローバル変数を追加する必要があります。
//+------------------------------------------------------------------+ //| Canvas Dashboard PART2.mq5 | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property strict //+------------------------------------------------------------------+ //| Canvas objects | //+------------------------------------------------------------------+ //--- CCanvas canvasText; // Declare text canvas //+------------------------------------------------------------------+ //| Canvas names | //+------------------------------------------------------------------+ //--- string canvasTextName = "TextCanvas"; // Set text canvas name //--- Added Extra inputs input bool EnableTextPanel = true; // Enable Text Panel input int TextPanelHeight = 200; // Text Panel Height input double TextBackgroundOpacityPercent = 85.0; // Text Background Opacity Percent input double StatsHeaderBgOpacityPercent = 20.0; // Stats Header Bg Opacity Percent input int StatsHeaderBgRadius = 8; // Stats Header Bg Radius //--- Extended the globals uint bg_pixels_text[]; //--- Store text background pixels int prev_mouse_state = 0; //--- Initialize previous mouse state int last_mouse_x = 0, last_mouse_y = 0; //--- Initialize last mouse positions int header_height = 27; //--- Set header height int gap_y = 7; //--- Set Y gap int button_size = 25; //--- Set button size int theme_x_offset = -75; //--- Set theme X offset int minimize_x_offset = -50; //--- Set minimize X offset int close_x_offset = -25; //--- Set close X offset bool panels_minimized = false; //--- Initialize panels minimized flag color HeaderColor = C'60,60,60'; //--- Set header color color HeaderHoverColor = clrRed; //--- Set header hover color color HeaderDragColor = clrMediumBlue; //--- Set header drag color
初めにおこなう拡張の実装として、新しいテキストパネル用に追加のCCanvasオブジェクトcanvasTextを宣言します。このオブジェクトはスクロール可能な利用ガイドを表示するために使用します。また、各キャンバスを一意に識別するために、文字列定数canvasTextNameを追加し、値をTextCanvasに設定します。これは他のキャンバスと同様の設計です。次に、テキストパネルをサポートするために入力パラメータを拡張します。EnableTextPanelはbool型でデフォルトtrueとし、テキストパネルの表示と非表示を切り替えるために使用します。TextPanelHeightは200に設定し、パネルの高さを制御します。TextBackgroundOpacityPercentは85.0とし、背景の透明度を調整します。StatsHeaderBgOpacityPercentは20.0とし、統計ヘッダー背景の透過率を制御します。StatsHeaderBgRadiusは8とし、ヘッダーの角丸半径を定義します。また、グラフや統計パネルと背景表現を統一するためにuint配列bg_pixels_textを追加し、テキストパネル用にスケーリングされた背景ピクセルデータを格納します。
次に、マウス操作に関するグローバル変数を定義および初期化します。prev_mouse_stateは0、last_mouse_xとlast_mouse_yもそれぞれ0で初期化し、マウスの移動追跡に使用します。header_heightは27、gap_yは7とし、レイアウト上の垂直間隔を定義します。button_sizeは25とします。ヘッダー右側に配置するボタンのオフセットとしてtheme_x_offsetは-75、minimize_x_offsetは-50、close_x_offsetは-25とします。またpanels_minimizedはfalseで初期状態は展開状態とします。HeaderColorを中間グレー、HeaderHoverColorを赤、HeaderDragColorをミディアムブルーとし、それぞれヘッダーの状態に応じて使用します。次のステップでは、テキストパネルに表示するコンテンツの定義と、それを制御するための追加グローバル変数を実装します。
string text_usage_text = "\nCanvas Dashboard Usage Guide\n\n" "Welcome to the Canvas Dashboard – Your Interactive Tool for Real-Time Market Monitoring in MetaTrader 5!\n\n" "Enhance your trading experience with this dynamic dashboard that visualizes price data, account stats, and interactive controls. Designed for ease of use, it allows customization through dragging, resizing, theme switching, and more, while providing essential market insights at a glance.\n\n" "Key Features:\n" "- Price Graph Panel: Displays recent bar closes with a fog gradient fill, background image support, and resize indicators.\n" "- Stats Panel: Shows account balance, equity, and current bar OHLC values with customizable backgrounds (single color or gradient).\n" "- Header Controls: Drag to move the dashboard; buttons for theme toggle (dark/light), minimize/maximize panels, and close.\n" "- Text Panel: Scrollable guide (this panel) with hover-expand scrollbar, up/down buttons, and slider for navigation.\n" "- Interactivity: Hover for highlights/tooltips; resize via borders (bottom, right, corner); wheel scroll in text area.\n" "- Theme Support: Switch between dark and light modes for better visibility on different chart backgrounds.\n" "- Background Options: Enable images with fog overlay; blend modes for transparency.\n\n" "Usage Instructions:\n" "1. Move the Dashboard: Click and drag the header (excluding buttons) to reposition on the chart.\n" "2. Resize Panels: Hover near the graph's bottom/right edges; click and drag when the icon appears (arrows for direction).\n" "3. Toggle Theme: Click the '[' icon in the header to switch between dark and light modes.\n" "4. Minimize/Maximize: Click the 'r' or 'o' icon to hide/show panels (header remains visible).\n" "5. Navigate Text: Use the scrollbar (hovers to expand with buttons/slider); click up/down or drag slider; wheel scroll in body.\n" "6. Close Dashboard: Click the 'X' icon in the header to remove it from the chart.\n\n" "Important Notes:\n" "- Risk Disclaimer: This dashboard is for informational purposes only. Always verify data and trade responsibly.\n" "- Compatibility Check: Ensure chart settings allow mouse events; test on demo for interactions.\n" "- Optimization Tips: Adjust input parameters like graph bars, fonts, colors, and opacity for your setup.\n" "- Security Measures: No account modifications; use on trusted platforms.\n" "- Legal Notice: No guarantees of accuracy. Consult professionals as needed.\n\n" "Contact Methods:\n" "NB:\n" "********************************************\n" " >*** FOR SUPPORT, QUERIES, OR CUSTOMIZATIONS, REACH OUT IMMEDIATELY: ***<\n" " __________________________________________\n\n" "1. Email: mutiiriallan.forex@gmail.com (Primary Support Channel)\n" "2. Telegram Channel: @ForexAlgo-Trader (Updates & Community)\n" "3. Telegram Group: https://t.me/Forex_Algo_Trader (Direct Assistance & Discussions)\n\n" "********************************************\n\n" "Thank you for choosing our Canvas Dashboard solutions. Use wisely, monitor confidently, and elevate your trading journey! 🚀\n"; //--- Set text usage content int text_scroll_pos = 0; //--- Initialize text scroll position int text_max_scroll = 0; //--- Initialize text max scroll int text_slider_height = 20; //--- Set text slider height bool text_movingStateSlider = false; //--- Initialize text slider moving flag int text_mlbDownY_Slider = 0; //--- Initialize text slider down Y int text_mlbDown_YD_Slider = 0; //--- Initialize text slider down YD int text_total_height = 0; //--- Initialize text total height int text_visible_height = 0; //--- Initialize text visible height bool text_scroll_visible = false; //--- Initialize text scroll visible flag bool text_mouse_in_body = false; //--- Initialize text mouse in body flag bool prev_text_mouse_in_body = false; //--- Initialize previous text mouse in body flag bool text_scroll_up_hovered = false; //--- Initialize text scroll up hovered flag bool text_scroll_down_hovered = false; //--- Initialize text scroll down hovered flag bool text_scroll_slider_hovered = false; //--- Initialize text scroll slider hovered flag bool text_scroll_area_hovered = false; //--- Initialize text scroll area hovered flag const int TEXT_MAX_LINES = 100; //--- Set text max lines int text_adjustedLineHeight = 0; //--- Initialize text adjusted line height int text_scrollbar_full_width = 16; //--- Set text scrollbar full width int text_scrollbar_thin_width = 2; //--- Set text scrollbar thin width int text_track_width = 16; //--- Set text track width int text_scrollbar_margin = 5; //--- Set text scrollbar margin int text_button_size = 16; //--- Set text button size color text_leader_color_dark = C'45,45,45'; //--- Set dark text leader color color text_leader_color_light = C'200,200,200'; //--- Set light text leader color color text_button_bg_dark = C'60,60,60'; //--- Set dark text button bg color text_button_bg_light = C'220,220,220'; //--- Set light text button bg color text_button_bg_hover_dark = C'70,70,70'; //--- Set dark text button bg hover color text_button_bg_hover_light = C'180,180,180'; //--- Set light text button bg hover color text_arrow_color_dark = C'150,150,150'; //--- Set dark text arrow color color text_arrow_color_light = C'50,50,50'; //--- Set light text arrow color color text_arrow_color_disabled_dark = C'80,80,80'; //--- Set dark disabled arrow color color text_arrow_color_disabled_light = C'150,150,150'; //--- Set light disabled arrow color color text_arrow_color_hover_dark = C'100,100,100'; //--- Set dark hover arrow color color text_arrow_color_hover_light = C'0,0,0'; //--- Set light hover arrow color color text_slider_bg_dark = C'80,80,80'; //--- Set dark slider bg color text_slider_bg_light = C'150,150,150'; //--- Set light slider bg color text_slider_bg_hover_dark = C'100,100,100'; //--- Set dark slider bg hover color text_slider_bg_hover_light = C'100,100,100'; //--- Set light slider bg hover color text_bg_light = clrWhite; //--- Set light text bg color text_bg_dark = clrBlack; //--- Set dark text bg color text_base_light = clrBlack; //--- Set light text base color text_base_dark = clrWhite; //--- Set dark text base
ここでは、文字列text_usage_textを定義し、テキストパネルに表示される使用ガイドの全コンテンツを格納します。このコンテンツは改行文字によって段落やセクションごとに構造化されており、ウェルカムメッセージ、パネル、コントロール、インタラクティビティ、テーマ、背景に関する主要機能の箇条書き、移動、サイズ変更、表示切替、ナビゲーション、クローズなどの使用手順を番号付きで整理したステップ、リスク、互換性、最適化、セキュリティ、法的事項を含む重要な注意事項、メールアドレスおよびTelegramの連絡先情報、そして最後のサンクスメッセージで構成されます。この構成により、ユーザーガイドとして必要な情報を一通り提供できます。あくまで一例のフォーマットとして必要に応じて自由に変更可能です。
次に、テキストスクロール管理用の変数を初期化します。text_scroll_posは現在のスクロール位置を保持するために0で初期化されます。text_max_scrollはスクロール可能な最大値を保持し、同様に0で初期化されます。text_slider_heightはスライダーサイズとして20に設定されます。text_movingStateSliderはスライダーのドラッグ状態を管理するフラグでありfalseで初期化されます。text_mlbDownY_Sliderおよびtext_mlbDown_YD_Sliderはスライダー操作時のマウス押下Y座標を保持するために0で初期化されます。text_total_heightとtext_visible_heightはそれぞれ全コンテンツ高さと表示領域高さを表し、初期値は0です。text_scroll_visibleはスクロールバーの表示制御フラグとしてfalseで初期化されます。text_mouse_in_bodyとprev_text_mouse_in_bodyはマウスがテキスト領域内にあるかどうかを追跡するためのフラグであり、どちらもfalseで初期化されます。さらに、text_scroll_up_hovered、text_scroll_down_hovered、text_scroll_slider_hovered、text_scroll_area_hoveredといったホバー状態フラグもすべてfalseで初期化されます。TEXT_MAX_LINESは100に設定し、描画ラインの上限バッファとして使用されます。またtext_adjustedLineHeightは行間を動的に調整するための値として0で初期化されます。次にスクロールバーの寸法を定義します。text_scrollbar_full_widthは展開時の幅として16、text_scrollbar_thin_widthは縮小時の幅として2に設定されます。text_track_widthはトラック幅として16、text_scrollbar_marginは余白として5、text_button_sizeは上下ボタンサイズとして16に設定されます。テーマカラーはC'...'形式で定義され、スクロールバー背景、ボタンの通常、ホバー状態、矢印の通常、無効、ホバー状態などを含みます。ダークモードとライトモードの両方に対応し、視認性と一貫性を確保する設計です。ベース背景色およびテキスト色もテーマに応じて白黒を切り替えることでコントラストを維持します。
次のステップでは、統計キャンバスに角丸矩形を描画する機能を追加するための関数を実装します。
//+------------------------------------------------------------------+ //| Fill rounded rectangle | //+------------------------------------------------------------------+ void FillRoundedRectangle(CCanvas &cvs, int x, int y, int w, int h, int radius, uint argb_color) { if (radius <= 0) { cvs.FillRectangle(x, y, x + w - 1, y + h - 1, argb_color); //--- Fill rectangle if no radius return; //--- Exit } radius = MathMin(radius, MathMin(w / 2, h / 2)); //--- Adjust radius cvs.Arc(x + radius, y + radius, radius, radius, DegreesToRadians(180), DegreesToRadians(90), argb_color); //--- Draw top-left arc cvs.Arc(x + w - radius - 1, y + radius, radius, radius, DegreesToRadians(270), DegreesToRadians(90), argb_color); //--- Draw top-right arc cvs.Arc(x + w - radius - 1, y + h - radius - 1, radius, radius, DegreesToRadians(0), DegreesToRadians(90), argb_color); //--- Draw bottom-right arc cvs.Arc(x + radius, y + h - radius - 1, radius, radius, DegreesToRadians(90), DegreesToRadians(90), argb_color); //--- Draw bottom-left arc cvs.FillCircle(x + radius, y + radius, radius, argb_color); //--- Fill top-left circle cvs.FillCircle(x + w - radius - 1, y + radius, radius, argb_color); //--- Fill top-right circle cvs.FillCircle(x + w - radius - 1, y + h - radius - 1, radius, argb_color); //--- Fill bottom-right circle cvs.FillCircle(x + radius, y + h - radius - 1, radius, argb_color); //--- Fill bottom-left circle cvs.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb_color); //--- Fill horizontal areas cvs.FillRectangle(x, y + radius, x + w - 1, y + h - radius - 1, argb_color); //--- Fill vertical areas }
FillRoundedRectangle関数を実装し、指定したCCanvas参照オブジェクト上に角丸矩形を描画します。この関数は、xおよびyの位置、幅wと高さh、角丸半径radius、ARGBカラーcolorをパラメータとして受け取ります。まず、radiusが0以下の場合は通常の矩形描画として処理し、(x,y)から(x+w-1, y+h-1)までをFillRectangleメソッドで塗りつぶした後、そこで処理を終了します。radiusが有効な場合は、まずMathMinを用いてradiusをw/2およびh/2の最小値に制限し、形状が破綻しないように調整します。次に各角に対して円弧を描画します。Arc関数を用いて四隅それぞれの90度セグメントを描きます。具体的には、左上は180度から270度、右上は270度から360度、右下は0度から90度、左下は90度から180度の範囲を描画します。これらの角度はDegreesToRadiansのようなヘルパー関数を使用してラジアンへ変換して使用します。このヘルパー関数は後ほど定義します。
次に、各角の中心位置でFillCircleを使用して完全な円を塗りつぶし、角丸領域を固めます。次に、横方向の中央ストリップを左側のradiusから右側の(w - radius)までの範囲で高さ全体にわたり塗りつぶし、縦方向の側面については上側のradiusから下側の(h - radius)までの範囲を幅全体にわたりFillRectangleで塗りつぶし、隙間なく形状を完成させます。補助的な変換関数は以下の通りです。
//+------------------------------------------------------------------+ //| Convert degrees to radians | //+------------------------------------------------------------------+ double DegreesToRadians(double degrees) { return((M_PI * degrees) / 180.0); //--- Perform conversion }
πまたはM_PIと対象の角度の乗算結果を180で割ることで計算をおこないます。これにより角度をラジアンへ変換できます。次に、この角丸矩形関数を使用して統計パネル内のヘッダーを描画するため、該当関数を更新します。
//+------------------------------------------------------------------+ //| Update stats on canvas | //+------------------------------------------------------------------+ void UpdateStatsOnCanvas() { //--- string headerText = "Account Stats"; //--- Set header text canvasStats.FontSet("Arial Bold", StatsHeaderFontSize); //--- Set font int textW = canvasStats.TextWidth(headerText); //--- Get text width int textH = canvasStats.TextHeight(headerText); //--- Get text height int pad = 5; //--- Set padding int rectX = (statsWidth - textW) / 2 - pad; //--- Calculate rect X int rectY = yPos - pad / 2; //--- Calculate rect Y int rectW = textW + 2 * pad; //--- Calculate rect width int rectH = textH + pad; //--- Calculate rect height uchar alpha = (uchar)(255 * (StatsHeaderBgOpacityPercent / 100.0)); //--- Calculate alpha uint argbHeaderBg = ColorToARGB(GetStatsHeaderBgColor(), alpha); //--- Convert bg to ARGB color header_border = is_dark_theme ? clrWhite : clrBlack; //--- Get border color uint argbHeaderBorder = ColorToARGB(header_border, 255); //--- Convert to ARGB FillRoundedRectangle(canvasStats, rectX - 1, rectY - 1, rectW + 2, rectH + 2, StatsHeaderBgRadius + 1, argbHeaderBorder); //--- Fill outer rectangle FillRoundedRectangle(canvasStats, rectX, rectY, rectW, rectH, StatsHeaderBgRadius, argbHeaderBg); //--- Fill inner rectangle uint argbHeader = ColorToARGB(headerColor, 255); //--- Convert header to ARGB canvasStats.TextOut(statsWidth / 2, yPos, headerText, argbHeader, TA_CENTER); //--- Draw header //--- }
変更を実装するために、UpdateStatsOnCanvas関数を修正し、ヘッダー描画を強化して角丸背景を追加することで、より洗練された外観にします。各ヘッダー(たとえばAccount StatsやCurrent Bar Stats)ごとに、フォントをArial Boldに設定し、StatsHeaderFontSizeをFontSetで指定します。次にTextWidthとTextHeightを使用してテキストの幅と高さを計測し、パディングを5ピクセルに設定します。そのうえで、パディングを含めた中央配置の矩形位置とサイズを計算します。xは(width - text width) / 2 - pad、yは現在のyPos - pad / 2、幅はtext + pad * 2、高さはtext + padとなります。
次に、StatsHeaderBgOpacityPercentを100.0で割った値に255を掛けてucharにキャストし、アルファ値を計算します。背景色はGetStatsHeaderBgColorから取得し、このアルファ値を用いてARGBへ変換します。枠線についてはダークテーマの場合は白、ライトテーマの場合は黒を選択し、完全不透明のARGB値に変換します。その後FillRoundedRectangleを2回呼び出します。1回目は外枠として位置を-1、サイズを+2、半径を+1として枠線ARGBで描画し、2回目はベース位置、サイズ、半径で背景ARGBを用いて描画します。ヘッダー色をアルファ255のARGB形式に変換し、stats幅を2で割った位置とyPosを基準としてテキストを中央揃えでTextOutを使用して描画します。その後、次のセクションとの間隔としてyPosを30増加させます。関数の残りの部分は以前定義した通りです。コンパイル後、次のような結果になります。

これにより、統計パネルのヘッダーが角丸矩形として描画されるようになります。次に実装するのはテキストパネルの描画です。他のパネルと同様に専用関数を作成しますが、その前にテキストを動的に折り返す必要があります。そのために、後で使用する補助関数群と併せてテキストラッピングを実装します。そのために、以下の手法を使用します。
//+------------------------------------------------------------------+ //| Lighten color | //+------------------------------------------------------------------+ color LightenColor(color colorValue, double factor) { int blue = (int)MathMin(255, (colorValue & 0xFF) * factor); //--- Lighten blue int green = (int)MathMin(255, ((colorValue >> 8) & 0xFF) * factor); //--- Lighten green int red = (int)MathMin(255, ((colorValue >> 16) & 0xFF) * factor); //--- Lighten red return (color)(blue | (green << 8) | (red << 16)); //--- Return lightened color } //+------------------------------------------------------------------+ //| Calculate slider height | //+------------------------------------------------------------------+ int TextCalculateSliderHeight() { int scroll_area_height = TextPanelHeight - 2 - 2 * text_button_size; //--- Calculate area height int slider_min_height = 20; //--- Set min height if (text_total_height <= text_visible_height) return scroll_area_height; //--- Return full if no scroll double visible_ratio = (double)text_visible_height / text_total_height; //--- Calculate ratio int height = (int)MathFloor(scroll_area_height * visible_ratio); //--- Calculate height return MathMax(slider_min_height, height); //--- Return max of min and calculated } //+------------------------------------------------------------------+ //| Scroll text up | //+------------------------------------------------------------------+ void TextScrollUp() { if (text_adjustedLineHeight > 0 && text_scroll_pos > 0) { text_scroll_pos = MathMax(0, text_scroll_pos - text_adjustedLineHeight); //--- Scroll up UpdateTextOnCanvas(); //--- Update canvas } } //+------------------------------------------------------------------+ //| Scroll text down | //+------------------------------------------------------------------+ void TextScrollDown() { if (text_adjustedLineHeight > 0 && text_scroll_pos < text_max_scroll) { text_scroll_pos = MathMin(text_max_scroll, text_scroll_pos + text_adjustedLineHeight); //--- Scroll down UpdateTextOnCanvas(); //--- Update canvas } } //+------------------------------------------------------------------+ //| Update text hover effects | //+------------------------------------------------------------------+ void TextUpdateHoverEffects(int local_x, int local_y) { if (!text_scroll_visible) return; //--- Exit if no scroll int scrollbar_x = canvasText.Width() - text_track_width - 1; //--- Get scrollbar X int scrollbar_y = 1; //--- Set scrollbar Y int scrollbar_height = TextPanelHeight - 2; //--- Get height text_scroll_up_hovered = text_scroll_area_hovered && (local_x >= scrollbar_x && local_x <= scrollbar_x + text_track_width - 1 && local_y >= scrollbar_y && local_y <= scrollbar_y + text_button_size - 1); //--- Check up hover int down_y = scrollbar_y + scrollbar_height - text_button_size; //--- Calculate down Y text_scroll_down_hovered = text_scroll_area_hovered && (local_x >= scrollbar_x && local_x <= scrollbar_x + text_track_width - 1 && local_y >= down_y && local_y <= down_y + text_button_size - 1); //--- Check down hover int scroll_area_height = scrollbar_height - 2 * text_button_size; //--- Calculate area height int slider_y = scrollbar_y + text_button_size + (int)(((double)text_scroll_pos / text_max_scroll) * (scroll_area_height - text_slider_height)); //--- Calculate slider Y text_scroll_slider_hovered = text_scroll_area_hovered && (local_x >= scrollbar_x && local_x <= scrollbar_x + text_track_width - 1 && local_y >= slider_y && local_y <= slider_y + text_slider_height - 1); //--- Check slider hover } //+------------------------------------------------------------------+ //| Get line color | //+------------------------------------------------------------------+ color GetLineColor(string lineText) { if (StringLen(lineText) == 0 || lineText == " ") return C'25,25,25'; //--- Return gray for empty if (StringFind(lineText, "mutiiriallan.forex@gmail.com") >= 0) return C'255,100,100'; //--- Return red for email if (StringFind(lineText, "https://t.me/Forex_Algo_Trader") >= 0) return C'150,100,200'; //--- Return purple for link if (StringFind(lineText, "@ForexAlgo-Trader") >= 0) return C'100,150,255'; //--- Return blue for channel if (StringFind(lineText, "http") >= 0 || StringFind(lineText, "t.me") >= 0) return C'100,150,255'; //--- Return blue for links string start3 = StringSubstr(lineText, 0, 3); //--- Get start substring if ((start3 == "1. " || start3 == "2. " || start3 == "3. " || start3 == "4. " || start3 == "5. ") && StringFind(lineText, "Initial Setup Instructions") < 0) return C'255,200,100'; //--- Return orange for numbered return clrWhite; //--- Return white default } //+------------------------------------------------------------------+ //| Check if heading | //+------------------------------------------------------------------+ bool IsHeading(string lineText) { if (StringLen(lineText) == 0) return false; //--- False for empty if (StringGetCharacter(lineText, StringLen(lineText) - 1) == ':') return true; //--- True for colon end if (StringFind(lineText, "Canvas Dashboard Usage Guide") >= 0) return true; //--- True for guide if (StringFind(lineText, "Key Features") >= 0) return true; //--- True for features if (StringFind(lineText, "Usage Instructions") >= 0) return true; //--- True for instructions if (StringFind(lineText, "Important Notes") >= 0) return true; //--- True for notes if (StringFind(lineText, "Contact Methods") >= 0) return true; //--- True for contacts if (StringFind(lineText, "NB:") >= 0) return true; //--- True for NB return false; //--- Return false } //+------------------------------------------------------------------+ //| Wrap text | //+------------------------------------------------------------------+ void WrapText(const string inputText, const string font, const int fontSize, const int maxWidth, string &wrappedLines[], color &wrappedColors[]) { ArrayResize(wrappedLines, 0); //--- Reset lines ArrayResize(wrappedColors, 0); //--- Reset colors string paragraphs[]; //--- Declare paragraphs int numParagraphs = StringSplit(inputText, '\n', paragraphs); //--- Split by newline for (int p = 0; p < numParagraphs; p++) { string para = paragraphs[p]; //--- Get paragraph color paraColor = GetLineColor(para); //--- Get color if (StringLen(para) == 0) { int size = ArraySize(wrappedLines); //--- Get size ArrayResize(wrappedLines, size + 1); //--- Resize wrappedLines[size] = " "; //--- Add space ArrayResize(wrappedColors, size + 1); //--- Resize colors wrappedColors[size] = C'25,25,25'; //--- Set gray continue; //--- Continue } string words[]; //--- Declare words int numWords = StringSplit(para, ' ', words); //--- Split by space string currentLine = ""; //--- Initialize line for (int w = 0; w < numWords; w++) { string testLine = currentLine + (StringLen(currentLine) > 0 ? " " : "") + words[w]; //--- Build test line canvasText.FontSet(font, fontSize); //--- Set font int textW = canvasText.TextWidth(testLine); //--- Get width if (textW <= maxWidth) { currentLine = testLine; //--- Update line } else { if (StringLen(currentLine) > 0) { int size = ArraySize(wrappedLines); //--- Get size ArrayResize(wrappedLines, size + 1); //--- Resize wrappedLines[size] = currentLine; //--- Add line ArrayResize(wrappedColors, size + 1); //--- Resize colors wrappedColors[size] = paraColor; //--- Set color } currentLine = words[w]; //--- Start new line } } if (StringLen(currentLine) > 0) { int size = ArraySize(wrappedLines); //--- Get size ArrayResize(wrappedLines, size + 1); //--- Resize wrappedLines[size] = currentLine; //--- Add last line ArrayResize(wrappedColors, size + 1); //--- Resize colors wrappedColors[size] = paraColor; //--- Set color } } }
いくつかのヘルパー関数を定義します。まず、LightenColor関数を実装します。この関数はcolorValueとdouble型のfactorを受け取り、RGB成分を抽出したうえでfactorを掛けることで色を明るくします。各成分はMathMinを用いて255を上限としてクランプし、オーバーフローを防ぎます。これはダークモードにおけるテキストカラーなど、テーマ調整用途で使用されます。colorValueとdouble型の係数を受け取り、ビットマスクとシフト演算を用いて青、緑、赤の各成分を抽出します。各成分に係数を掛け、その結果をintにキャストしたうえでMathMinを使用して255を上限として制限します。その後、各成分をビットシフトとOR演算で結合し、最終的なcolor型として再構成します。
次に、TextCalculateSliderHeight関数を作成します。この関数は、表示領域に対する全体コンテンツ量の比率に基づいてスクロールバーのスライダーサイズを計算します。最低サイズを保証することで操作性を維持します。まずtext_button_sizeを用いて上下ボタンを含めた領域からTextPanelHeightを引き、実際のスライダー可能領域を算出します。最小値は20とし、スクロールが不要な場合はスライダーサイズを全領域として返します。スクロールが必要な場合はvisibleとtotalの比率に応じてarea * visible/totalを計算し、最小値と比較して大きい方を返します。次にTextScrollUpを定義します。この関数はtext_scroll_posをtext_adjustedLineHeight分だけ減少させることで上方向へスクロールします。ただし0未満にならないよう制限し、更新後はUpdateTextOnCanvasを呼び出して再描画します。同様にTextScrollDownはtext_scroll_posを1行分増加させ、text_max_scrollを超えないよう制限したうえでUpdateTextOnCanvasを呼び出します。
TextUpdateHoverEffects関数では、スクロールバー各部分のホバー状態を検出します。スクロールバーが非表示の場合は即時終了します。スクロールバーの位置とサイズを計算し、テキスト領域内であることを条件として、上ボタン領域ではtext_scroll_up_hovered、下ボタン領域ではtext_scroll_down_hovered、スライダー領域ではtext_scroll_slider_hoveredを更新します。またtext_scroll_area_hoveredも併せて判定に使用します。GetLineColor関数では行内容に応じた色分類をおこないます。空行またはスペースのみの場合は灰色、emailアドレスは赤、特定リンクやチャンネルやhttp、t.meを含む場合は紫または青、番号付きリストは橙、それ以外の特定パターンに該当しない場合は白をデフォルトとします。IsHeading関数では見出し判定をおこないます。空行はfalse、末尾がコロンの場合やguide、features、instructions、notes、contacts、NBなどの特定キーワードに一致する場合はtrueとします。なお、この識別は必要に応じてHTML的な形式を用いることも可能ですが、ここでは簡易的なルールベース判定を採用しています。様々なアイデアがあるので、自分に合ったものを選んでください。
最後に、WrapTextは入力テキストを幅制約に基づいて複数行へ分割し、それぞれの行に対応する色情報を付与します。出力用の配列を初期化し、まず改行でテキストを段落に分割します。各段落ごとに色を取得し、空行の場合はスペースを追加して灰色として扱います。次に、各段落を単語単位に分割し、行を構築します。新しい単語を追加した場合のテスト行を作成し、テンポラリフォントを設定した状態でTextWidthを用いて幅を測定します。収まる場合は現在の行に追加し、収まらない場合は現在の行を確定して出力配列に追加し、新しい行として開始します。すべての単語処理後、残っている行がある場合はそれを出力配列に追加します。これらのヘルパー関数により、WrapTextはテキストを幅制約に従って分割し、色情報を保持したまま行データへ変換します。
//+------------------------------------------------------------------+ //| Update text on canvas | //+------------------------------------------------------------------+ void UpdateTextOnCanvas() { canvasText.Erase(0); //--- Clear text canvas int textWidth = canvasText.Width(); //--- Get text width int textHeight = TextPanelHeight; //--- Get text height color text_bg = is_dark_theme ? text_bg_dark : text_bg_light; //--- Get bg color uint argb_bg = ColorToARGB(text_bg, (uchar)(255 * (TextBackgroundOpacityPercent / 100.0))); //--- Convert to ARGB canvasText.FillRectangle(0, 0, textWidth - 1, textHeight - 1, argb_bg); //--- Fill background uint argbBorder = ColorToARGB(GetBorderColor(), 255); //--- Convert border canvasText.Line(0, 0, textWidth - 1, 0, argbBorder); //--- Draw top canvasText.Line(textWidth - 1, 0, textWidth - 1, textHeight - 1, argbBorder); //--- Draw right canvasText.Line(textWidth - 1, textHeight - 1, 0, textHeight - 1, argbBorder); //--- Draw bottom canvasText.Line(0, textHeight - 1, 0, 0, argbBorder); //--- Draw left int padding = 10; //--- Set padding int textAreaX = 1 + padding; //--- Calculate area X int textAreaY = 1; //--- Set area Y int textAreaWidth = textWidth - 2 - padding * 2; //--- Calculate area width int textAreaHeight = textHeight - 2; //--- Calculate area height string font = "Arial"; //--- Set font int fontSize = 16; //--- Set font size canvasText.FontSet(font, fontSize); //--- Set font int lineHeight = canvasText.TextHeight("A"); //--- Get line height text_adjustedLineHeight = lineHeight + 3; //--- Adjust line height text_visible_height = textAreaHeight; //--- Set visible height static string wrappedLines[]; //--- Declare wrapped lines static color wrappedColors[]; //--- Declare wrapped colors static bool wrapped = false; //--- Initialize wrapped flag bool need_scroll = false; //--- Initialize scroll need int reserved_width = 0; //--- Initialize reserved width if (!wrapped) { WrapText(text_usage_text, font, fontSize, textAreaWidth, wrappedLines, wrappedColors); //--- Wrap text wrapped = true; //--- Set wrapped flag } int numLines = ArraySize(wrappedLines); //--- Get number of lines text_total_height = numLines * text_adjustedLineHeight; //--- Calculate total height need_scroll = text_total_height > text_visible_height; //--- Check if scroll needed if (need_scroll) { reserved_width = text_track_width; //--- Set reserved width textAreaWidth -= reserved_width; //--- Adjust area width WrapText(text_usage_text, font, fontSize, textAreaWidth, wrappedLines, wrappedColors); //--- Rewrap text numLines = ArraySize(wrappedLines); //--- Update num lines text_total_height = numLines * text_adjustedLineHeight; //--- Update total height } text_max_scroll = MathMax(0, text_total_height - text_visible_height); //--- Calculate max scroll text_scroll_visible = need_scroll; //--- Set scroll visible text_scroll_pos = MathMax(0, MathMin(text_scroll_pos, text_max_scroll)); //--- Adjust scroll pos if (text_scroll_visible) { int scrollbar_y = 1; //--- Set scrollbar Y int scrollbar_height = textHeight - 2; //--- Calculate scrollbar height int scroll_area_height = scrollbar_height - 2 * text_button_size; //--- Calculate area height text_slider_height = TextCalculateSliderHeight(); //--- Calculate slider height int scrollbar_x = textWidth - text_track_width - 1; //--- Calculate scrollbar X color leader_color = is_dark_theme ? text_leader_color_dark : text_leader_color_light; //--- Get leader color uint argb_leader = ColorToARGB(leader_color, 255); //--- Convert to ARGB canvasText.FillRectangle(scrollbar_x, scrollbar_y, scrollbar_x + text_track_width - 1, scrollbar_y + scrollbar_height - 1, argb_leader); //--- Fill leader int slider_y = scrollbar_y + text_button_size + (int)(((double)text_scroll_pos / text_max_scroll) * (scroll_area_height - text_slider_height)); //--- Calculate slider Y if (text_scroll_area_hovered) { color button_bg = is_dark_theme ? text_button_bg_dark : text_button_bg_light; //--- Get button bg color button_bg_hover = is_dark_theme ? text_button_bg_hover_dark : text_button_bg_hover_light; //--- Get hover bg color up_bg = text_scroll_up_hovered ? button_bg_hover : button_bg; //--- Determine up bg uint argb_up_bg = ColorToARGB(up_bg, 255); //--- Convert to ARGB canvasText.FillRectangle(scrollbar_x, scrollbar_y, scrollbar_x + text_track_width - 1, scrollbar_y + text_button_size - 1, argb_up_bg); //--- Fill up button color arrow_color = is_dark_theme ? text_arrow_color_dark : text_arrow_color_light; //--- Get arrow color color arrow_color_disabled = is_dark_theme ? text_arrow_color_disabled_dark : text_arrow_color_disabled_light; //--- Get disabled arrow color arrow_color_hover = is_dark_theme ? text_arrow_color_hover_dark : text_arrow_color_hover_light; //--- Get hover arrow color up_arrow = (text_scroll_pos == 0) ? arrow_color_disabled : (text_scroll_up_hovered ? arrow_color_hover : arrow_color); //--- Determine up arrow color uint argb_up_arrow = ColorToARGB(up_arrow, 255); //--- Convert to ARGB canvasText.FontSet("Webdings", 22); //--- Set font int arrow_x = scrollbar_x + text_track_width / 2; //--- Calculate arrow X int arrow_y = scrollbar_y + (text_button_size / 2) - (canvasText.TextHeight(CharToString(0x35)) / 2); //--- Calculate arrow Y canvasText.TextOut(arrow_x, arrow_y, CharToString(0x35), argb_up_arrow, TA_CENTER); //--- Draw up arrow int down_y = scrollbar_y + scrollbar_height - text_button_size; //--- Calculate down Y color down_bg = text_scroll_down_hovered ? button_bg_hover : button_bg; //--- Determine down bg uint argb_down_bg = ColorToARGB(down_bg, 255); //--- Convert to ARGB canvasText.FillRectangle(scrollbar_x, down_y, scrollbar_x + text_track_width - 1, down_y + text_button_size - 1, argb_down_bg); //--- Fill down button color down_arrow = (text_scroll_pos >= text_max_scroll) ? arrow_color_disabled : (text_scroll_down_hovered ? arrow_color_hover : arrow_color); //--- Determine down arrow uint argb_down_arrow = ColorToARGB(down_arrow, 255); //--- Convert to ARGB int down_arrow_x = scrollbar_x + text_track_width / 2; //--- Calculate down arrow X int down_arrow_y = down_y + (text_button_size / 2) - (canvasText.TextHeight(CharToString(0x36)) / 2); //--- Calculate down arrow Y canvasText.TextOut(down_arrow_x, down_arrow_y, CharToString(0x36), argb_down_arrow, TA_CENTER); //--- Draw down arrow int slider_x = scrollbar_x + text_scrollbar_margin; //--- Calculate slider X int slider_w = text_track_width - 2 * text_scrollbar_margin; //--- Calculate slider width int cap_radius = slider_w / 2; //--- Calculate cap radius color slider_bg_color = is_dark_theme ? text_slider_bg_dark : text_slider_bg_light; //--- Get slider bg color slider_bg_hover_color = is_dark_theme ? text_slider_bg_hover_dark : text_slider_bg_hover_light; //--- Get hover bg color slider_bg = text_scroll_slider_hovered || text_movingStateSlider ? slider_bg_hover_color : slider_bg_color; //--- Determine bg uint argb_slider = ColorToARGB(slider_bg, 255); //--- Convert to ARGB canvasText.Arc(slider_x + cap_radius, slider_y + cap_radius, cap_radius, cap_radius, DegreesToRadians(180), DegreesToRadians(360), argb_slider); //--- Draw top arc canvasText.FillCircle(slider_x + cap_radius, slider_y + cap_radius, cap_radius, argb_slider); //--- Fill top circle canvasText.Arc(slider_x + cap_radius, slider_y + text_slider_height - cap_radius, cap_radius, cap_radius, DegreesToRadians(0), DegreesToRadians(180), argb_slider); //--- Draw bottom arc canvasText.FillCircle(slider_x + cap_radius, slider_y + text_slider_height - cap_radius, cap_radius, argb_slider); //--- Fill bottom circle canvasText.FillRectangle(slider_x, slider_y + cap_radius, slider_x + slider_w, slider_y + text_slider_height - cap_radius, argb_slider); //--- Fill middle } else { int thin_w = text_scrollbar_thin_width; //--- Set thin width int thin_x = scrollbar_x + (text_track_width - thin_w) / 2; //--- Calculate thin X int cap_radius = thin_w / 2; //--- Calculate cap radius color slider_bg_color = is_dark_theme ? text_slider_bg_dark : text_slider_bg_light; //--- Get bg uint argb_slider = ColorToARGB(slider_bg_color, 255); //--- Convert to ARGB canvasText.Arc(thin_x + cap_radius, slider_y + cap_radius, cap_radius, cap_radius, DegreesToRadians(180), DegreesToRadians(360), argb_slider); //--- Draw top canvasText.FillCircle(thin_x + cap_radius, slider_y + cap_radius, cap_radius, argb_slider); //--- Fill top canvasText.Arc(thin_x + cap_radius, slider_y + text_slider_height - cap_radius, cap_radius, cap_radius, DegreesToRadians(0), DegreesToRadians(180), argb_slider); //--- Draw bottom canvasText.FillCircle(thin_x + cap_radius, slider_y + text_slider_height - cap_radius, cap_radius, argb_slider); //--- Fill bottom canvasText.FillRectangle(thin_x, slider_y + cap_radius, thin_x + thin_w, slider_y + text_slider_height - cap_radius, argb_slider); //--- Fill middle } } color text_base = is_dark_theme ? text_base_dark : text_base_light; //--- Get base color for (int line = 0; line < numLines; line++) { string lineText = wrappedLines[line]; //--- Get line text if (StringLen(lineText) == 0) continue; //--- Skip empty color lineColor = wrappedColors[line]; //--- Get line color if (is_dark_theme) lineColor = (lineColor == clrWhite) ? clrWhite : LightenColor(lineColor, 1.5); //--- Adjust for dark else lineColor = (lineColor == clrWhite) ? clrBlack : DarkenColor(lineColor, 0.7); //--- Adjust for light int line_y = textAreaY + line * text_adjustedLineHeight - text_scroll_pos; //--- Calculate line Y if (line_y + text_adjustedLineHeight < 0 || line_y > textAreaHeight) continue; //--- Skip out of view if (IsHeading(lineText)) { canvasText.FontSet("Arial Bold", fontSize); //--- Set bold font lineColor = clrDodgerBlue; //--- Set heading color } else canvasText.FontSet(font, fontSize); //--- Set regular font uint argbText = ColorToARGB(lineColor, 255); //--- Convert to ARGB canvasText.TextOut(textAreaX, line_y, lineText, argbText, TA_LEFT); //--- Draw line } canvasText.Update(); //--- Update text canvas }
ここでは、UpdateTextOnCanvas関数を実装し、canvasTextオブジェクト上にスクロール可能なテキストコンテンツを描画します。この処理では、背景、枠線、動的なテキストの折り返し、テーマに応じた文字色調整、さらにホバー効果付きのカスタムスクロールバーによるナビゲーションを扱います。Eraseをゼロに設定してキャンバスをクリアし、現在の幅/高さを取得し、テーマの背景色(暗いまたは明るい)を決定し、TextBackgroundOpacityPercentから不透明度を100.0×255で割った値をucharにキャストしてARGBに変換し、FillRectangleメソッドを使用して(0,0)から幅/高さマイナス1までの矩形を塗りつぶします。続いて、GetBorderColorから枠線の色を取得し、アルファ255のARGBへ変換して、上下左右の辺をLineで描画します。
パディングは10に設定し、テキスト領域をx=1+padding、y=1、幅は全体幅から枠と左右のパディングを引いた値、高さは枠線を除いた値として計算します。フォントは Arial のサイズ16に設定し、FontSetを使用します。TextHeightで文字「A」の高さから行の高さを取得し、text_adjustedLineHeightに3を加えて行間を調整します。表示可能な高さはtext_visible_heightとして領域の高さを設定します。テキストがまだ折り返されていない場合(静的変数wrappedがfalseの場合)は、WrapText関数を呼び出し、テキスト内容、フォント、サイズ、領域幅を渡してwrappedLinesとwrappedColorsに格納し、その後wrappedをtrueに設定します。行数を配列サイズから取得し、text_total_heightは行数×調整済み行高さとして計算します。総高さが表示可能領域を超えている場合、スクロールが必要かどうかをチェックします。必要であればreserved_widthをtext_track_widthに設定し、領域幅を減らし、テキストを再度折り返しし直して、行数および総高さを更新します。
総高さが表示可能領域を超えている場合、スクロールが必要かどうかをチェックします。必要であればreserved_widthをtext_track_widthに設定し、領域幅を減らし、テキストを再度折り返し直して、行数および総高さを更新します。text_max_scrollは0と(総高さから表示可能高さを引いた値)のうち大きい方として計算します。text_scroll_visibleはスクロールが必要かどうかneed_scrollに設定し、text_scroll_posは0からtext_max_scrollの範囲にクランプします。表示される場合、スクロールバーの位置を計算します。Yは1、高さはテキスト領域から枠を除いた値であり、さらにtext_button_sizeを2倍した分を引いた領域がスライダーの可動範囲になります。text_slider_heightはTextCalculateSliderHeightから取得し、Xは幅からトラック幅と1を引いた位置になります。テーマのリーダー色leader colorを取得し、ARGBに変換してトラックの矩形を塗りつぶします。スライダーのY位置は、スクロール位置の割合に(可動範囲からスライダー高さを引いた値)を掛けて算出します。text_scroll_area_hoveredがtrueであれば、テーマのボタン背景色およびホバー色を取得し、text_scroll_up_hoveredがtrueの場合は上ボタンの背景を決定し、ARGBに変換して上ボタン矩形を塗りつぶします。矢印の色も取得します。通常、無効、ホバーの色を取得し、上方向の矢印色は位置が0のときは無効色、それ以外はホバーまたは通常色になります。ARGBに変換し、Webdingsフォントをサイズ22で設定し、矢印の中心座標を計算し、中央揃えで上矢印0x35をTextOutで描画します。同様に下ボタンについても処理します。下ボタンのYは下端に配置され、背景色やホバー色を取得して塗りつぶします。下矢印の色は位置が最大値のときは無効色、それ以外はホバーまたは通常色になります。中央揃えで下矢印0x36を描画します。
スライダーについては、Xをxにmarginを加えた位置として計算し、幅wはトラック幅からマージンを2倍引いたものになります。キャップ半径はwの半分です。テーマのスライダー背景色およびホバー色を取得し、ホバー中またはドラッグ中である場合に背景を決定し、ARGBに変換します。DegreesToRadiansを用いて角度をラジアンに変換しながら、上端と下端の円弧や円をArcおよびFillCircleで描画し、中間の矩形を塗りつぶします。ホバーしていない場合は、細いスライダーを中央のX位置に描画します。幅は細く、キャップ半径は半分で、テーマ色をARGBに変換して使用します。上端と下端の円弧と円を描画し、細い中央部分を塗りつぶします。
次にテキスト描画をおこないます。テーマのベースカラーを取得し、wrappedLinesをループします。空行はスキップします。色を取得して調整します。白または黒の場合はそのまま、それ以外はダークテーマでは1.5倍明るくし、ライトテーマでは0.7倍暗くします。各行のY位置はarea yにline × adjustedLineHeightを加え、そこからtext_scroll_posを引いた値として計算し、表示領域外(負の値または領域を超える場合)はスキップします。IsHeadingがtrueの場合は太字フォントとドジャーブルー色を設定し、それ以外は通常フォントを使用します。ARGBに変換した後、左揃えでテキストを描画します。最後に、Updateを使ってキャンバスを更新し、テキストを表示します。この関数はキャンバス領域を定義した後の初期化処理の中で呼び出されます。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Other panels logic if (EnableTextPanel) { int textY = currentCanvasY + header_height + gap_y + currentHeight + PanelGap; //--- Calculate text Y if (!canvasText.CreateBitmapLabel(0, 0, canvasTextName, currentCanvasX, textY, header_width, TextPanelHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Failed to create Text Canvas"); //--- Log text creation failure } textCreated = true; //--- Set text created flag } if (EnableTextPanel) UpdateTextOnCanvas(); //--- Update text if enabled ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true); //--- Enable mouse wheel events ChartRedraw(); //--- Redraw chart return(INIT_SUCCEEDED); //--- Return success } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if (textCreated) canvasText.Destroy(); //--- Destroy text if created ChartRedraw(); //--- Redraw chart } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { static datetime lastBarTime = 0; //--- Initialize last time datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Get current time if (currentBarTime > lastBarTime) { UpdateGraphOnCanvas(); //--- Update graph if (EnableStatsPanel) UpdateStatsOnCanvas(); //--- Update stats if (EnableTextPanel) UpdateTextOnCanvas(); //--- Update text ChartRedraw(); //--- Redraw chart lastBarTime = currentBarTime; //--- Update last time } }
まず、 OnInitイベントハンドラを修正し、EnableTextPanelがtrueの場合にテキストパネルの作成を組み込みます。他のパネルのロジックの後で、y位置をcurrentCanvasYにheader_height、gap_y、currentHeight、PanelGapを加えた値として計算し、CreateBitmapLabelを使用してサブウィンドウ0にビットマップラベルを作成します。名前はcanvasTextName、位置とヘッダー幅、TextPanelHeight、およびCOLOR_FORMAT_ARGB_NORMALIZEを指定します。作成に失敗した場合はエラーを出力し、成功時にはtextCreatedをtrueに設定します。さらに「EnableTextPanel」が有効な場合、「UpdateTextOnCanvas」を呼び出して初期描画をおこないます。チャートのホイールイベントを検知するためにChartSetIntegerを使用し、CHART_EVENT_MOUSE_WHEELをtrueに設定してマウスホイールイベントを有効化します。これによりスクロール操作の検出が可能になります。その後ChartRedrawを呼び出し、INIT_SUCCEEDEDを返します。
次にOnDeinitイベントハンドラを更新し、textCreatedがtrueの場合に限りcanvasText.Destroyを呼び出してテキストキャンバスを破棄し、その後再描画をおこないます。OnTickイベントハンドラも調整し、新しいバー発生時にテキストパネルの更新を含めます。static変数「lastBarTime」を0で初期化し、iTimeを使用して現在のバー時間を取得します。これが前回より大きい場合、新しいバーと判断し、「UpdateGraphOnCanvas」を呼び出し、「EnableStatsPanel」が有効であれば「UpdateStatsOnCanvas」も呼び出し、「EnableTextPanel」が有効であれば「UpdateTextOnCanvas」も呼び出します。その後ChartRedrawを実行し、lastBarTimeを更新します。コンパイルすると、次の結果が得られます。

画像から確認できるように、テキストパネルが正常に描画されています。次に必要なのは、現在描画しているテキストのスクロールを処理するためにチャートイベントハンドラを更新することです。以下がそのロジックです。ここでは最終結果を得るためにおこなった変更のみを記載しており、以前のバージョンで実装していた他のロジックは削除しています。
//+------------------------------------------------------------------+ //| Chart event handler | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if (id == CHARTEVENT_CHART_CHANGE) { //--- Existing logic if (EnableTextPanel) UpdateTextOnCanvas(); //--- Update text ChartRedraw(); //--- Redraw chart } else if (id == CHARTEVENT_MOUSE_MOVE) { //--- Existing logic if (EnableTextPanel && !panels_minimized) { int text_canvas_x = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XDISTANCE); //--- Get text X int text_canvas_y = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YDISTANCE); //--- Get text Y int text_canvas_w = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XSIZE); //--- Get width int text_canvas_h = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YSIZE); //--- Get height bool is_over_text = (mouse_x >= text_canvas_x && mouse_x <= text_canvas_x + text_canvas_w && mouse_y >= text_canvas_y && mouse_y <= text_canvas_y + text_canvas_h); //--- Check over text bool prev_scroll_hovered = text_scroll_area_hovered; //--- Store prev scroll hover text_scroll_area_hovered = false; //--- Reset area hover if (is_over_text) { int local_x = mouse_x - text_canvas_x; //--- Get local X int local_y = mouse_y - text_canvas_y; //--- Get local Y if (local_x >= text_canvas_w - text_track_width - 1) { text_scroll_area_hovered = true; //--- Set area hover } bool prev_up = text_scroll_up_hovered; //--- Store prev up bool prev_down = text_scroll_down_hovered; //--- Store prev down bool prev_slider = text_scroll_slider_hovered; //--- Store prev slider TextUpdateHoverEffects(local_x, local_y); //--- Update hovers if (prev_scroll_hovered != text_scroll_area_hovered || prev_up != text_scroll_up_hovered || prev_down != text_scroll_down_hovered || prev_slider != text_scroll_slider_hovered) { UpdateTextOnCanvas(); //--- Update text ChartRedraw(); //--- Redraw chart } text_mouse_in_body = (local_x < text_canvas_w - text_track_width - 1); //--- Set body flag } else { bool need_redraw = prev_scroll_hovered || text_scroll_up_hovered || text_scroll_down_hovered || text_scroll_slider_hovered; //--- Check redraw need if (need_redraw) { text_scroll_area_hovered = false; //--- Reset area text_scroll_up_hovered = false; //--- Reset up text_scroll_down_hovered = false; //--- Reset down text_scroll_slider_hovered = false; //--- Reset slider UpdateTextOnCanvas(); //--- Update text ChartRedraw(); //--- Redraw chart } text_mouse_in_body = false; //--- Reset body flag } // New: Toggle CHART_MOUSE_SCROLL on enter/leave text body if (text_mouse_in_body != prev_text_mouse_in_body) { ChartSetInteger(0, CHART_MOUSE_SCROLL, !text_mouse_in_body); //--- Toggle scroll prev_text_mouse_in_body = text_mouse_in_body; //--- Update prev } } if (mouse_state == 1 && prev_mouse_state == 0) { //--- Existing logic if (EnableTextPanel && !panels_minimized && text_scroll_visible && text_scroll_area_hovered) { int text_canvas_x = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XDISTANCE); //--- Get text X int text_canvas_y = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YDISTANCE); //--- Get text Y int local_x = mouse_x - text_canvas_x; //--- Get local X int local_y = mouse_y - text_canvas_y; //--- Get local Y int scrollbar_x = canvasText.Width() - text_track_width - 1; //--- Get scrollbar X int scrollbar_y = 1; //--- Set scrollbar Y int scrollbar_height = TextPanelHeight - 2; //--- Get height int scroll_area_y = scrollbar_y + text_button_size; //--- Calculate area Y int scroll_area_height = scrollbar_height - 2 * text_button_size; //--- Calculate area height int slider_y = scroll_area_y + (int)(((double)text_scroll_pos / text_max_scroll) * (scroll_area_height - text_slider_height)); //--- Calculate slider Y if (local_x >= scrollbar_x && local_x <= scrollbar_x + text_track_width - 1) { if (local_y >= scrollbar_y && local_y <= scrollbar_y + text_button_size - 1) { TextScrollUp(); //--- Scroll up } else if (local_y >= scrollbar_y + scrollbar_height - text_button_size && local_y <= scrollbar_y + scrollbar_height - 1) { TextScrollDown(); //--- Scroll down } else if (local_y >= scroll_area_y && local_y <= scroll_area_y + scroll_area_height - 1) { if (local_y >= slider_y && local_y <= slider_y + text_slider_height - 1) { text_movingStateSlider = true; //--- Set moving slider text_mlbDownY_Slider = local_y; //--- Set down Y text_mlbDown_YD_Slider = slider_y; //--- Set down YD ChartSetInteger(0, CHART_MOUSE_SCROLL, false); //--- Disable scroll } else { int new_slider_y = local_y - text_slider_height / 2; //--- Calculate new Y new_slider_y = MathMax(scroll_area_y, MathMin(new_slider_y, scroll_area_y + scroll_area_height - text_slider_height)); //--- Clamp Y double ratio = (double)(new_slider_y - scroll_area_y) / (scroll_area_height - text_slider_height); //--- Calculate ratio text_scroll_pos = (int)MathRound(ratio * text_max_scroll); //--- Update pos } UpdateTextOnCanvas(); //--- Update text ChartRedraw(); //--- Redraw chart } } } } else if (text_movingStateSlider && mouse_state == 1) { int text_canvas_y = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YDISTANCE); //--- Get text Y int local_y = mouse_y - text_canvas_y; //--- Get local Y int delta_y = local_y - text_mlbDownY_Slider; //--- Calculate delta Y int new_slider_y = text_mlbDown_YD_Slider + delta_y; //--- Calculate new slider Y int scrollbar_y = 1; //--- Set scrollbar Y int scrollbar_height = TextPanelHeight - 2; //--- Get height int slider_min_y = scrollbar_y + text_button_size; //--- Calculate min Y int slider_max_y = scrollbar_y + scrollbar_height - text_button_size - text_slider_height; //--- Calculate max Y new_slider_y = MathMax(slider_min_y, MathMin(new_slider_y, slider_max_y)); //--- Clamp Y double scroll_ratio = (double)(new_slider_y - slider_min_y) / (slider_max_y - slider_min_y); //--- Calculate ratio int new_scroll_pos = (int)MathRound(scroll_ratio * text_max_scroll); //--- Calculate new pos if (new_scroll_pos != text_scroll_pos) { text_scroll_pos = new_scroll_pos; //--- Update pos UpdateTextOnCanvas(); //--- Update text ChartRedraw(); //--- Redraw chart } } else if (mouse_state == 0 && prev_mouse_state == 1) { //--- Existing logic if (text_movingStateSlider) { text_movingStateSlider = false; //--- Reset slider moving ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable scroll UpdateTextOnCanvas(); //--- Update text ChartRedraw(); //--- Redraw chart } } last_mouse_x = mouse_x; //--- Update last X last_mouse_y = mouse_y; //--- Update last Y prev_mouse_state = mouse_state; //--- Update prev state } else if (id == CHARTEVENT_MOUSE_WHEEL) { int flg_keys = (int)(lparam >> 32); //--- Get keys int mx = (int)(short)lparam; //--- Get X int my = (int)(short)(lparam >> 16); //--- Get Y int delta = (int)dparam; //--- Get delta if (EnableTextPanel && !panels_minimized && text_scroll_visible) { int text_canvas_x = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XDISTANCE); //--- Get text X int text_canvas_y = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YDISTANCE); //--- Get text Y int text_canvas_w = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XSIZE); //--- Get width int text_canvas_h = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YSIZE); //--- Get height bool is_over_text_body = (mx >= text_canvas_x && mx <= text_canvas_x + text_canvas_w - text_track_width && my >= text_canvas_y && my <= text_canvas_y + text_canvas_h); //--- Check over body if (is_over_text_body) { int scroll_step = 20; //--- Set step text_scroll_pos += (delta > 0 ? -scroll_step : scroll_step); //--- Adjust pos text_scroll_pos = MathMax(0, MathMin(text_scroll_pos, text_max_scroll)); //--- Clamp pos UpdateTextOnCanvas(); //--- Update text int current_scale = (int)ChartGetInteger(0, CHART_SCALE); //--- Get scale int adjust = (delta > 0 ? 1 : -1); // Swap to (delta > 0 ? -1 : 1) if wheel direction is opposite int revert_scale = current_scale + adjust; //--- Calculate revert revert_scale = MathMax(0, MathMin(5, revert_scale)); //--- Clamp scale ChartSetInteger(0, CHART_SCALE, revert_scale); //--- Set scale ChartRedraw(); //--- Redraw chart } } } }
ここでは、 OnChartEventイベントハンドラを修正し、EnableTextPanelがtrueかつpanels_minimizedではない場合にテキストパネルのインタラクティブ機能を追加します。ObjectGetIntegerを使用してテキストキャンバスの位置とサイズを取得します。取得するプロパティはOBJPROP_XDISTANCE、OBJPROP_YDISTANCE、OBJPROP_XSIZE、OBJPROP_YSIZEです。その後、マウスがテキスト領域全体の上にあるかを判定します。前回のtext_scroll_area_hoveredの状態を保存し、いったんfalseにリセットします。マウスが領域内にある場合、マウス座標からキャンバス位置を引いてローカル座標を計算します。ローカルxがスクロールバー領域、つまり幅からtext_track_widthと1を引いた位置から右端までの範囲にある場合、スクロールバー領域上にあると判定します。
前回の上ボタン、下ボタン、スライダーのホバー状態を保存し、TextUpdateHoverEffectsをローカル座標で呼び出してホバー状態を更新します。ホバー状態または領域状態に変化があった場合はUpdateTextOnCanvasを呼び出して再描画します。ローカルxがスクロールバーより左側の場合は、text_mouse_in_bodyをtrueに設定します。一方でテキスト領域外にあり、かつ前回の状態変化により更新が必要な場合は、領域、上ボタン、下ボタン、スライダーのホバー状態をすべてfalseにリセットし、テキストを更新して再描画します。text_mouse_in_bodyが前回値から変化した場合は、ChartSetIntegerを使用してCHART_MOUSE_SCROLLを切り替えます。テキスト内ではfalse、テキスト外ではtrueに設定し、チャートのズーム操作とホイール操作の干渉を防ぎます。その後、前回状態を更新します。
マウスダウン処理です。stateが1でpreviousが0の場合、テキストパネルが有効で、最小化されておらず、スクロールバー領域がホバーされている場合に処理をおこないます。ローカルx、ローカルy、スクロールバーのx、y、高さ、エリアy、高さ、スライダーyを取得します。ローカルxがスクロールバー領域内にある場合、ローカルyが上ボタン領域であればTextScrollUpを呼び出し、下ボタン領域であればTextScrollDownを呼び出します。それ以外の場合でエリア内にある場合、スライダー上であればtext_movingStateSliderをtrueに設定し、text_mlbDownY_Sliderにローカルyを保存し、text_mlbDown_YD_Sliderにスライダーyを保存し、通常スクロールを無効化します。スライダー上でない場合は、クリック位置から新しいスライダー位置を計算します。ローカルyからtext_slider_heightの半分を引いた値を基準にし、スライダーの移動範囲であるarea yからarea y+height-text_slider_heightの間にクランプします。その後ratioを(new-area y) divided by(height-text_slider_height)として計算し、text_scroll_posをrounded ratio times text_max_scrollに設定します。そしてテキスト更新と再描画をおこないます。
次にドラッグ中の処理です。text_movingStateSliderがtrueでstateが1(押下継続)の場合、マウスのローカルyを取得し、開始位置との差分deltaを計算します。new slider yをtext_mlbDown_YD_Slider+deltaとして算出し、これをスライダー範囲にクランプします。最小値はy+text_button_size、最大値はy+height-text_button_size-text_slider_heightです。ratioを(new-min) divided by(max-min)として計算し、新しい位置をrounded ratio times text_max_scrollとして算出します。現在のtext_scroll_posと異なる場合のみ更新し、テキスト更新と再描画をおこないます。
マウスアップ処理ではstateが0でpreviousが1の場合、text_movingStateSliderがfalseであればスクロールを有効に戻し、テキスト更新と再描画をおこないます。さらにチャート変更イベントでは、EnableTextPanelがtrueの場合、再描画前にUpdateTextOnCanvasを呼び出す処理を追加します。マウスホイールイベントでは、lparamからflg_keysをshift 32で取得し、mxとmyをshortとして抽出し、dparamをdeltaとして整数として扱います。text scroll visibleがtrueの場合に処理をおこない、マウスがテキスト本体領域内にあるかを判定します。テキストまたはスクロールが表示可能な状態であれば、テキストのプロパティを取得し、マウスが本体領域上にあるかを判定します(mx,myがxからx + width - text_track_widthの範囲かつyからy+heightの範囲内)。もし該当する場合、text_scroll_posをホイールの入力に応じて20ずつ増減させ(deltaが正なら減少、負なら増加)、0からtext_max_scrollの範囲にクランプします。その後テキストを更新します。さらにChartGetIntegerを使用して現在のチャートスケール(CHART_SCALE)を取得し、ホイール方向に応じて+1または-1で調整し、0から5の範囲にクランプします。そしてChartSetIntegerで再設定することでズーム動作を抑制します。最後に再描画をおこないます。コンパイルすると、次の結果が得られます。

可視化結果から、すべてのインタラクションを備えたテキストパネルを追加することで、キャンバスベースのダッシュボードが拡張されていることが確認できます。残るは、システムの動作確認、つまり前のセクションでおこなったテストです。
バックテスト
テストを実施しました。以下はコンパイル後の可視化を単一のGraphics Interchange Format (GIF)ビットマップ画像形式で示したものです。

結論
結論として、MQL5におけるcanvasベースの価格ダッシュボードは、使用ガイド用のピクセルパーフェクトなスクロール可能テキストキャンバスを実装することで強化されました。これにより、MQL5標準機能の制約を回避し、アンチエイリアスによる滑らかな描画、ホバー時に拡張される角丸カスタムスクロールバー、ボタンとスライダーによるナビゲーション、テーマ対応要素、色調整を伴う動的なテキスト折り返し、さらにホイール、クリック、ドラッグ操作への対応が実現されています。このシステムはテキストパネルを既存のグラフおよび統計表示と統合し、ドラッグ、サイズ変更、テーマ変更、最小化機能を維持しながら、効率的なイベント駆動更新によって包括的なモニタリングツールとして機能します。このピクセルパーフェクトなスクロールテキストキャンバス拡張により、ダッシュボード内で詳細なユーザーガイダンスを提供できるようになり、取引環境におけるさらなる最適化の準備が整いました。取引をお楽しみください。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/21072
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
MQL5でカスタムインジケータを作成する(第6回):平滑化、色相シフト、マルチタイムフレーム対応を備えたRSI計算の拡張
MQL5入門(第37回):MQL5のAPIとWebRequest関数の習得(XI)
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
MQL5標準ライブラリエクスプローラー(第6回):生成されたエキスパートアドバイザーの最適化
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索