Produced by Fourier

Electronでタイトルバーをカスタマイズする方法

Hirayama Hirayama カレンダーアイコン 2024.11.22

最近 Electron を使用したアプリ開発をする機会があり、その際UI改善の為にWindowフレームの見た目を改良しようとしました。

公式のドキュメント を見ると結構丁寧にコード例が書かれて助かりましたが、ちゃんと仕上げるにあたり苦労したため、どのように対応したかを簡単にまとめました。

GitHubにサンプルアプリケーションのリポジトリを作成したので、実装について気になる方はそちらを参照していただければと思います。

GitHub - FOURIER-Inc/electron-frameless-window: A simple example of an Electron app with a frameless window. thumbnail

GitHub - FOURIER-Inc/electron-frameless-window: A simple example of an Electron app with a frameless window.

A simple example of an Electron app with a frameless window. - FOURIER-Inc/electron-frameless-window

https://github.com/FOURIER-Inc/electron-frameless-window

前提条件

本記事を書くにあたり、サンプルアプリケーションを作成する際に Electron Forge を使用しました。

実行した新規アプリケーションの作成コマンドは以下の通りです。

npm init electron-app@latest my-app -- --template=vite-typescript

インストール後、 npm start コマンドを実行して表示されるアプリは以下のようになります(アプリ起動後Dev Toolsを非表示)。

本記事では、この状態からタイトルバーをカスタマイズしていきます。

なお、本記事で扱うOSはWindowsとmacOSのみでLinuxは対象外です。ただ、LinuxのウィンドウフレームはWindowsと似ている部分が多いので、Windowsで作り込むことができたら、ほぼそのままLinuxにも適用できると思います。

また、本記事で作るタイトルバーの目標として、同じくElectronで作られているSlackのようなタイトルバーにしようと思います。

Windowsの場合

macOSの場合

Slackはタイトルバーの位置にボタンや検索バーが配置されており、Web版と同様の見た目をしています。

1. 標準のタイトルバーを非表示にする

昨今のモダンなアプリケーションはほぼ必ずと言っていいほどタイトルバーの見た目がカスタマイズされているためか、Electronのドキュメントにもカスタマイズ方法が記載されています。

そのため、まずは公式のやり方を参考にタイトルバーをカスタマイズします。

https://www.electronjs.org/docs/latest/tutorial/window-customization

まずは、標準のタイトルバーを非表示にするため、 BrowserWindow をインスタンス化する際に、 titleBarStyletitleBarOverlay を以下のように設定します。

const mainWindow = new BrowserWindow({
  titleBarStyle: 'hidden',
  titleBarOverlay: true,
});

この状態でアプリケーションを起動すると、以下のように表示されます。

背景が真っ白なので分かりにくいですが、これでタイトルバーが消え、Webコンテンツがウィンドウ全体に描画されるようになりました。ただ、タイトルバーが消えたため、ウィンドウを掴んで移動することができなくなってしまいました。

2. タイトルバーを表示する

このままだとアプリとして非常に使いにくいので、タイトルバーを出してウィンドウをドラッグできるようにします。

タイトルバーにはHTMLの <header> 要素を使用しますが、普段のWebコーディングの知識だけでは、ドラッグをどのように有効化すればいいのかといった問題や、ウィンドウコントロール(Windowsだと右上、macOSは左上)との高さや色が合わなかったり、表示が被ったりする見た目の問題が出てきます。

これをどのようにして解消するのかというと、公式ドキュメントの Window Controls Overlay の項目にもある通り、 Window Controls Overlay API によって使用できるパラメータを使用することで、解消するようにします。

ドラッグできない問題の解消

これを解消するのは比較的簡単で、公式ドキュメントの Set custom draggable region にある通り、CSSで -webkit-app-region: drag; を対象の要素に対して使用することで、ドラッグできるようになります。

header {
  -webkit-app-region: drag;
}

ただ、上記の場合、タイトルバーにあるボタンも含めて全てドラッグ可能要素になってしまい、配置されたボタン等が押せなくなってしまうため、 no-drag で例外領域を指定します。

header {
  -webkit-app-region: drag;

  & button {
    -webkit-app-region: no-drag;
  }
}

見た目の整え方

タイトルバーの見た目は、ウィンドウコントロールボタンの部分と、 <header> 要素によって描画される部分の二ヶ所を整える必要があります。

1. ウィンドウコントロールボタン

ウィンドウコントロールボタンの部分は、 BrowerWindow をインスタンス化する際、 titleBarOverlay オブジェクトに colorsymbolColorheight を設定することで調整可能です。なお、これらの内、 colorsymbolColor はWindowsとLinuxで有効で、macOSは背景( color )が透明でボタンの色( symbolColor )は信号機カラーになります。

これらのパラメータの値はアプリケーションのデザインなどによって異なりますが、macOSのウィンドウコントロールボタンの背景が透明になることや、モーダルダイアログでバックドロップ(オーバーレイ)を表示する際、ウィンドウコントロールボタンだけ色が変わらないのを避けるため、 color プロパティは rgba(0, 0, 0, 0) にして透明にすることをお勧めします。

2. <header> 要素

<header> 要素の見た目を変える際は、 WICGのドキュメント を参考にします。このドキュメントはPWAアプリケーション関連のドキュメントですが、Electronにも同じ知識が適用可能です。

上記のドキュメントから、タイトルバー領域(左上を原点とした場合の、ウィンドウコントロールを除いたタイトルバーが表示される領域)に関連する変数は以下の4種があります。

変数名 意味
titlebar-area-x タイトルバー領域のX方向のオフセット値
titlebar-area-y タイトルバー領域のY方向のオフセット値
titlebar-area-width タイトルバー領域の幅
titlebar-area-height タイトルバー領域の高さ

これらの変数は env() 関数によって使用することができ、値の単位は全てピクセル(px)です。使用方法は height: env(titlebar-area-height, 2em); のように記述し、第二引数にフォールバック用の値を入れることができます。

この時に注意するのがOSごとによる値の違いで、Windowsにおけるタイトルバーの幅( titlebar-area-x )は、ウィンドウ幅とウィンドウコントロールの幅の差、タイトルバーのオフセットは無しですが、macOSの場合、タイトルバーの幅は同じものの、ウィンドウコントロールが左上にあるので、X方向のオフセット( titlebar-area-x )があります。

これらの変数や仕様を併せて表示しようとする場合、様々な実装が考えられますが、私は以下のようにして実装することにしました。

  • <header> 要素の width を100%にして、ウィンドウと同じサイズにする

    これはmacOSのウィンドウコントロールボタンの背景が透明になることへの対策です。Windowsの場合は、タイトルバーがウィンドウコントロールボタンの下に描画され隠れるようになりますが、前述したウィンドウコントロールボタンの背景色を透明にしていた場合、macOSと同じように表示されます。

  • <header> 要素の子要素に <div> 要素を入れ、 <div> 要素の widthtitlebar-area-width の値と同じにする

    1の実装上、そのまま <header> 要素に子要素を配置すると、ウィンドウコントロールボタンと子要素が被るため、 <div> 要素でオフセットをして被らないようにします。

  • <header> 要素の padding-lefttitlebar-area-width の値と同じにする

    これも同様の理由で、ウィンドウコントロールボタンと被らないようにするための対策です。

    💡
    あまり詳しくは調べていませんが、アラビア語などの書字方向が異なる環境を考慮する場合、ウィンドウコントロールボタンの配置も左右入れ替わる可能性があるため、 padding-inline-start で記述する方が適切かもしれません。
  • <div> 要素の min-heightmax-height を100%にする

    通常であれば、コンテンツの高さに応じて親要素(タイトルバー)の高さが変わる方が望ましいですが、今回はウィンドウコントロールボタンの高さとのずれが生じないようにするため、強制的に高さを一定に維持し、コンテンツの方が大きかった場合、潰れて表示されるようにしています。

3. 適用

上記の内容を全て適用したコードは以下のようになります。 確認のため、 <header> 要素の両端のボタンとダイアログを追加しています。

const mainWindow = new BrowserWindow({
  titleBarStyle: 'hidden',
  titleBarOverlay: {
    color: 'rgba(0, 0, 0, 0)',
    symbolColor: '#ffffff',
    height: 30,
  },
});
main.ts
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World!</title>

  </head>
  <body>
    <header>
      <div>
        <button>left</button>
        <button>right</button>
      </div>
    </header>

    <h1>💖 Hello World!</h1>
    <p>Welcome to your Electron application.</p>

    <button id="open-dialog">show dialog</button>

    <dialog>
      <h1>Dialog</h1>
      <p>Here is a dialog element</p>
      <button id="close-dialog">close</button>
    </dialog>
    
    <script type="module" src="/src/renderer.ts"></script>
  </body>
</html>
index.html
* {
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
    Arial, sans-serif;
  margin: 0;
}

header {
  position: sticky;
  top: env(titlebar-area-y, 0);
  height: env(titlebar-area-height, 30px);
  padding-left: env(titlebar-area-x, 0);
  -webkit-app-region: drag;
  -webkit-user-select: none;
  background-color: #1f1f1f;

  & > div {
    display: flex;
    justify-content: space-between;
    width: env(titlebar-area-width, 100%);
    min-height: 100%;
    max-height: 100%;
    padding-inline: 1em;
    padding-block: 0.2em;

    & * {
      -webkit-app-region: no-drag;
    }
  }
}

dialog {
  &::backdrop {
    background-color: rgba(143, 74, 255, 0.5);
  }
}
index.css
// dialog
// copied from https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog

const dialog = document.querySelector("dialog");
const showButton = document.querySelector("#open-dialog");
const closeButton = document.querySelector("#close-dialog");

showButton.addEventListener("click", () => {
  dialog.showModal();
});

closeButton.addEventListener("click", () => {
  dialog.close();
});

Windows

macOS

まとめ

本記事では、タイトルバーの見た目の変更方法について以下の要素を説明しました。

  1. BrowserWindow をインスタンス化する際に渡すパラメータの変更
    1. WindowsとmacOSで異なるウィンドウコントロールボタンの描画の説明
    2. バックドロップを考慮した色設定
  2. ドラッグ可能要素の指定方法及び除外方法
  3. タイトルバー領域についての説明と変数の使用方法

本記事の内容で、タイトルバーをカスタマイズする際の問題と解決策は概ね網羅されていると思います。 内容自体は分かってしまえば簡単ですが、最初はこれらの情報が集まらなかったり、思った通りの見た目が表示されず苦労したので、本記事が参考になれば幸いです。

最後に、冒頭にも書きましたが、本記事のリポジトリをGitHubにて公開しているので、実装を参考にしたい方はそちらもぜひご覧ください。

GitHub - FOURIER-Inc/electron-frameless-window: A simple example of an Electron app with a frameless window. thumbnail

GitHub - FOURIER-Inc/electron-frameless-window: A simple example of an Electron app with a frameless window.

A simple example of an Electron app with a frameless window. - FOURIER-Inc/electron-frameless-window

https://github.com/FOURIER-Inc/electron-frameless-window

Hirayama

Hirayama slash forward icon Engineer

業務では主にPHPやTypeScriptを使用したバックエンドアプリケーションやデスクトップアプリケーションの開発をしています。趣味は登山。