This article will show you how to quickly implement a title bar in Electron that can be rolled up, dragged around,
and take you further through the Electron bugs.
Hide the default title bar
Change properties in the main.ts(or main.js) where you create the BrowserWindow like below:
function createWindow() {
if (prod) Menu.setApplicationMenu(null);
mainWindow = new BrowserWindow({
titleBarStyle: dev ? 'default' : 'hiddenInset',
titleBarOverlay: true,
frame: false,
// ...
Make a title bar div
Now that the default title bar is gone, you should write a div that will serve as your own title bar.
This div is no different from any other div, except that it will support three things:
- the ability to drag
- can be rolled up
- have a traffic light component (roll up/down, maximize, minimize, close)
<div className="title-bar">
<div className="logo-and-name"><img src="public/assets/icon.ico" alt="logo" />My Application</div>
<div className="traffic-light">
<MinMaxClose /> <!-- MinMaxClose You can implement it yourself, four buttons, roll up/down, maximize, minimize, close -->
</div>
</div>
Make it draggable
Make a Drag component
A Drag component that has all its children draggable.
import * as React from "react";
import { HTMLAttributes } from "react";
import nconsole from "_rutils/nconsole";
interface DragProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
function Drag(props: DragProps) {
const { children, ...rest } = props;
const stopMove = () => {
window.ipcAPI?.initMoveWindow(false);
};
const startMove = (e: React.SyntheticEvent<HTMLDivElement>) => {
let elementDraggable = true;
if (e.target instanceof HTMLInputElement // input, buttons, etc. can not be dragged, you can freely add components that do not want to be dragged.
|| e.target instanceof HTMLButtonElement
|| e.target instanceof HTMLTextAreaElement
) {
elementDraggable = false;
}
if (elementDraggable) {
window.ipcAPI?.initMoveWindow(true);
window.ipcAPI?.moveWindow();
}
};
return (
<div
{...rest}
onMouseDown={(e) => startMove(e)}
onMouseUp={(e) => stopMove()}
>
{ children }
</div>
);
}
export default Drag;
So what are window.ipcAPI?.initMoveWindow(true); window.ipcAPI?.moveWindow(); ?
function initMoveWindow(moveState: boolean) {
ipcRenderer.send('window-move-init', moveState);
}
function moveWindow() {
ipcRenderer.send('window-move');
}
See it below.
Using ipcMain to Control Window Movement
Let’s take a look at the window-move-init and window-move code.
window-move-initinvokes when you click on the title bar, it tells electron: “Prepare to move!”window-moveinvokes when your mouse moves every frame
Put the following code into your main.ts or any other location where you can call ipcMain.on.
let winStartPosition = { x: 0, y: 0 };
let mouseStartPosition = { x: 0, y: 0 };
let size = [0, 0];
let ready2move = false;
let movingInterval: string | number | NodeJS.Timeout | null | undefined;
ipcMain.on("window-move-init", (e, moveState: boolean) => {
if (moveState) {
const winPosition = win.getPosition();
winStartPosition = { x: winPosition[0], y: winPosition[1] };
mouseStartPosition = screen.getCursorScreenPoint();
size = win.getSize();
} else {
if (movingInterval) clearInterval(movingInterval);
movingInterval = null;
}
ready2move = moveState;
});
ipcMain.on("window-move", (e) => {
if (ready2move) {
if (movingInterval) {
clearInterval(movingInterval);
}
// setInterval is used to solve the problem of mouseMove events being lost when the mouse moves out of the window too quickly.
movingInterval = setInterval(() => {
// real-time location updates
const cursorPosition = screen.getCursorScreenPoint();
const x = winStartPosition.x + cursorPosition.x - mouseStartPosition.x;
const y = winStartPosition.y + cursorPosition.y - mouseStartPosition.y;
// You have to use setBounds, not setPosition, otherwise the window will slowly get bigger,
// which is what I mean by electron's bug, which is still not fixed!
win.setBounds({ // win is your mainWindow, which is not reflected in this example.
x,
y,
width: size[0],
height: size[1],
});
}, 1); // 1ms doesn't slow down your app's performance.
} else {
if (movingInterval) clearInterval(movingInterval);
movingInterval = null;
}
});
Eventually, to prevent unforeseen problems, you should cancel dragging when you right-click or press ESC
export function APP() {
useEffect(() => {
const dragFallBack = (e: KeyboardEvent) => {
if (e.key === "Escape") {
window.ipcAPI?.initMoveWindow(false);
}
};
window.addEventListener("keydown", dragFallBack);
return () => {
window.removeEventListener("keydown", dragFallBack);
};
}, []);
return (
<section style={{ height: "100%" }} onContextMenu={() => window.ipcAPI?.initMoveWindow(false)}>
)
}
Okay, now try the drag effect!
Roll up
Rolling up is simpler and works the same way as dragging, also using ipcMain to control the window.
The difference is that rollup and down use the same button, so you should keep track of whether you are currently rolled up or down.
Renderer
<div className="traffic-light"> <!-- Add roll up/down, maximize/minimize, close logic to title bar -->
<MinMaxClose
scrollButtonStatus={trafficLightScrollButtonStatus}
onScrollClick={() => {
window.ipcAPI?.titleScrollToggle();
}}
onMinimize={() => {
window.ipcAPI?.mainWindowControl('minimize');
}}
onMaximize={() => window.ipcAPI?.mainWindowControl('maximize')}
onClose={() => alertAndClose()}
/>
</div>
ipcMain code
let sizeForScroll = [0, 0];
let alreadyScrollUp = false;
ipcMain.handle("is-window-maximized", (e) => {
return win.isMaximized();
});
ipcMain.handle("is-window-scrolled-up", (e) => {
return win.getSize()[1] <= 50;
});
const scroll = (method: string) => {
if (method === "up") {
if (alreadyScrollUp) {
return;
}
sizeForScroll = win.getSize();
// logger.log("current size: " + sizeForScroll[0] + ", " + sizeForScroll[1]);
win.setSize(sizeForScroll[0], titleBarHeight, true);
alreadyScrollUp = true;
return;
}
alreadyScrollUp = false;
// Uses the current width, and height before roll-up
win.setSize(win.getSize()[0], sizeForScroll[1], true);
};
// when mouse scrolls, which will be called multiple times instantly
ipcMain.on("title-scroll", (e, method: string) => {
scroll(method);
});
// when button clicked
ipcMain.on("title-scroll-toggle", (e) => {
if (alreadyScrollUp) {
// scroll down
alreadyScrollUp = false;
// Uses the current width, and height before roll-up
win.setSize(win.getSize()[0], sizeForScroll[1], true);
return;
}
// scroll up
sizeForScroll = win.getSize();
// logger.log("current size: " + sizeForScroll[0] + ", " + sizeForScroll[1]);
win.setSize(sizeForScroll[0], titleBarHeight, true);
alreadyScrollUp = true;
});
Done
OK. The above is not the complete code, but it contains an idea of how to solve the problem.
At this point, you should be able to achieve the effect in the cover image.