TCP Socket Router 예제를 통한 Electron Application 작성방법에 대한 연재가 완료되었습니다.
Electron + Vue.js 애플리케이션 만들기
1. Electron 프로젝트 생성하기(Create Electron Application Project)
2. Electron IPC 통신(Electron Architecture, IPC Main/IPC Renderer)
3. TCP Router 기능 구현(Implements TCP Router Communication)
5. 환경설정 구현하기(Preferences Window)
6. 메뉴 사용하기(Application Menu & Context Menu)
7. 시스템 트레이 사용 및 창 최소화(System Tray & Minimize to Tray)
8. Bootstrap Vue와 Font Awesome 사용하기(Using Bootstrap Vue & Font Awesome)
9. Dark Layout과 Frameless Window(Dark Layout & Frameless Window)
10. 빌드 및 배포, 자동 업데이트(Build, Auto Updater)
이번 포스트에서는 Dark Layout과 Frameless Window를 구현합니다.
Dark Layout 적용하기
본 프로젝트에서는 bootswatch 모듈을 추가하여 간단하게 적용하였습니다.
레이아웃 코드 적용하기
Bootstrap 테마를 지원하는 bootswatch를 프로젝트에 추가합니다.
npm install --save bootswatch
/src/renderer/App.vue의 <style lang="scss">에 다음을 추가합니다.
// @import "~bootswatch/dist/[theme]/variables";
// @import "~bootstrap/scss/bootstrap";
// @import "~bootswatch/dist/[theme]/bootswatch";
@import "~bootswatch/dist/slate/variables";
@import "~bootstrap/scss/bootstrap";
@import "~bootswatch/dist/slate/bootswatch";
적용 화면 미리 보기
Frameless Window 창으로 변경하기
창 관리 Component 생성하기
먼저, 아이콘 및 창 이름, 최소화 버튼, 최대화 버튼, 닫기 버튼을 관리하기 위한 Component를 생성합니다. 아래의 창은 각각의 창 윗부분에 추가하여 공통적으로 사용합니다. 창이 초기화될 때 창의 정보를 읽어와서 버튼의 활성화 및 창 이름을 적용합니다.
Frameless Window 사용 시에 창의 이동은 html의 속성에 -webkit-app-region: drag; 또는 -webkit-app-region: no-drag; 로 제어 가능합니다. 마우스 이벤트를 사용하는 요소에서는 no-drag를 사용하고 나머지 구간에서는 drag 속성으로 정의하여 내용 부분을 제외한 나머지 부분에서 창의 이동이 가능해집니다. 아래의 코드에서는 창 상단의 창 이름 부분에만 이동이 가능하도록 구현하였습니다.
<template>
<b-card-header style="-webkit-app-region: drag; color: #fff;">
<img class="icon" src="~@/assets/apps.png" /> {{ title }}
<b-button-group class="window-controls-container">
<b-button @click="handleMinimize" size="sm" variant="outline-primary" class="window-icon-bg" v-if="minimizable"><div class="window-icon window-minimize"></div></b-button>
<b-button @click="handleMaximize" size="sm" variant="outline-primary" class="window-icon-bg" v-if="maximizable && !maximized"><div class="window-icon window-maximize"></div></b-button>
<b-button @click="handleMaximize" size="sm" variant="outline-primary" class="window-icon-bg" v-if="maximizable && maximized"><div class="window-icon window-unmaximize"></div></b-button>
<b-button @click="handleClose" size="sm" variant="outline-danger" class="window-icon-bg window-close-bg"><div class="window-icon window-close"></div></b-button>
</b-button-group>
</b-card-header>
</template>
<script>
import { ipcRenderer, remote } from 'electron';
export default {
name: 'dialog-header',
components: {
},
props: {
name : {
type : String,
required: true
}
},
data() {
return {
isDarwin : process.platform === "darwin",
maximizable: false,
minimizable: false,
resizable: false,
maximized: false,
title : "",
}
},
created() {
this.title = this.$route.meta.title;
ipcRenderer.once("window-state", (event, args) => {
console.log(args);
this.minimizable = args.minimizable;
this.maximizable = args.maximizable;
this.resizable = args.resizable;
});
ipcRenderer.send(this.name + "-window-state");
let window = remote.getCurrentWindow();
window.on('maximize', () => {
setTimeout( () => {
this.maximized = true;
}, 100);
});
window.on('unmaximize', () => {
setTimeout( () => {
this.maximized = false;
}, 100);
});
},
methods: {
handleMinimize(e) {
const window = remote.getCurrentWindow();
window.minimize();
},
handleMaximize(e) {
const window = remote.getCurrentWindow();
if (!this.maximized) {
this.maximized = true;
window.maximize();
} else {
this.maximized = false;
window.unmaximize();
}
},
handleClose(e) {
const window = remote.getCurrentWindow();
window.close();
},
}
}
</script>
<style scoped>
.icon {
width: 16px;
height: 16px;
margin-left:-6px; margin-top:-2px;
}
.window-controls-container {
display: flex;
/* flex-grow: 0; */
/* flex-shrink: 0; */
/* text-align: center; */
/* position: relative; */
/* z-index: 3000; */
-webkit-app-region: no-drag;
height: 26px;
/* width: 138px; */
margin-left: auto;
/* margin-right: 1rem; */
margin-top: -0.72rem;
float: right;
}
.window-controls-container>.window-icon-bg {
display: inline-block;
-webkit-app-region: no-drag;
height: 100%;
/* width: 33.34%; */
width: 45px;
border: 1px solid #ccc;
border-top: 0;
border-right: 0;
padding: 0;
}
.window-controls-container>.window-icon-bg:first-child {
/* border-right: 0; */
border-top-left-radius: 0;
/* border-bottom-left-radius: 3px; */
}
.window-controls-container>.window-icon-bg:last-child {
border-right: 1px solid #ccc;
border-top-right-radius: 0;
/* border-bottom-right-radius: 3px; */
}
.window-controls-container .window-icon svg {
shape-rendering: crispEdges;
text-align: center
}
.window-controls-container .window-close {
mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.279 5.5L11 10.221l-.779.779L5.5 6.279.779 11 0 10.221 4.721 5.5 0 .779.779 0 5.5 4.721 10.221 0 11 .779 6.279 5.5z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%;
-webkit-mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.279 5.5L11 10.221l-.779.779L5.5 6.279.779 11 0 10.221 4.721 5.5 0 .779.779 0 5.5 4.721 10.221 0 11 .779 6.279 5.5z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%;
}
.window-controls-container .window-unmaximize {
mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 8.798H8.798V11H0V2.202h2.202V0H11v8.798zm-3.298-5.5h-6.6v6.6h6.6v-6.6zM9.9 1.1H3.298v1.101h5.5v5.5h1.1v-6.6z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%;
-webkit-mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 8.798H8.798V11H0V2.202h2.202V0H11v8.798zm-3.298-5.5h-6.6v6.6h6.6v-6.6zM9.9 1.1H3.298v1.101h5.5v5.5h1.1v-6.6z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%;
}
.window-controls-container .window-maximize {
mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 0v11H0V0h11zM9.899 1.101H1.1V9.9h8.8V1.1z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%;
-webkit-mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 0v11H0V0h11zM9.899 1.101H1.1V9.9h8.8V1.1z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%;
}
.window-controls-container .window-minimize {
mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 4.399V5.5H0V4.399h11z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%;
-webkit-mask: url("data:image/svg+xml;charset=utf-8,%3Csvg width='11' height='11' viewBox='0 0 11 11' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 4.399V5.5H0V4.399h11z' fill='%23000'/%3E%3C/svg%3E") no-repeat 50% 50%;
}
.window-controls-container .window-icon-bg>.window-icon {
height: 100%;
width: 100%;
mask-size: 23.1%;
-webkit-mask-size: 23.1%;
background-color: #cccccc;
}
.window-controls-container .window-icon-bg:hover {
background-color: hsla(0,0%,100%,.1);
}
.window-controls-container .window-icon-bg:hover {
background-color: rgba(0,0,0,.3);
}
.window-controls-container .window-icon-bg.window-close-bg:hover {
background-color: rgba(232,17,35,.8);
}
.window-controls-container .window-icon.window-close:hover {
background-color: #fff;
}
</style>
창 정보 불러오기
각각의 창을 관리하는 main 프로세스의 코드에 아래와 같이 각각의 창 정보를 반환하는 코드를 삽입합니다. BrowserWindow 생성 시 옵션에 frame: false를 추가하여 프레임을 제거합니다.
/src/main/window/main-window.js 에서 창 생성 시에 아래의 코드를 추가합니다.
......(상단생략)......
const style = {
......
frame: false,
......
};
......(중간생략)......
ipcMain.on("main-window-state", (event, data) => {
event.sender.send("window-state", {
minimizable: style.minimizable,
maximizable: style.maximizable,
resizable: style.resizable,
});
});
......(하단생략)......
/src/main/window/preferences-window.js 에서 창 생성시에 아래의 코드를 추가합니다.
......(상단생략)......
const style = {
......
frame: false,
......
};
......(중간생략)......
ipcMain.on("preferences-window-state", (event, data) => {
event.sender.send("window-state", {
minimizable: style.minimizable,
maximizable: style.maximizable,
resizable: style.resizable,
});
});
......(하단생략)......
/src/main/window/fontawesome-window.js 에서 창 생성시에 아래의 코드를 추가합니다.
......(상단생략)......
const style = {
......
frame: false,
......
};
......(중간생략)......
ipcMain.on("fontawesome-window-state", (event, data) => {
event.sender.send("window-state", {
minimizable: style.minimizable,
maximizable: style.maximizable,
resizable: style.resizable,
});
});
......(하단생략)......
창 관리 Component 사용하기
/src/renderer/views/MainPage.vue에 DialogHeader를 추가합니다. (다른 창도 아래와 같이 등록합니다. DialogHeader의 이름을 적절하게 수정합니다.)
<template>
<b-card no-body class="h-100 main-wrapper" @click="clearActiveItem">
<DialogHeader name="main"></DialogHeader>
......(중간생략)......
<script>
const DialogHeader = require('../components/DialogHeader').default;
export default {
name: 'main-page',
components: {
DialogHeader
},
......(이하생략)......
적용 화면 미리 보기
메인 화면
환경설정 화면
아이콘 목록 모두 보기 화면
참고자료
- Bootswatch
https://bootswatch.com/
소스코드
본 포스트 관련 소스코드는 여기에서 다운로드 가능합니다.
'Development > Node.js, Vue.js, Electron.js' 카테고리의 다른 글
D3.js Geo Map on Vue.js(D3.js를 이용하여 지도 그리기) (4) | 2020.04.21 |
---|---|
[TCP Socket Router #10] Build, Distribute and Auto Updater (2) | 2020.03.05 |
[TCP Socket Router #08] Using Bootstrap Vue, Font Awesome (0) | 2020.03.02 |
[TCP Socket Router #07] System Tray & Minimize to Tray (0) | 2020.02.28 |
[TCP Socket Router #06] Application Menu & Context Menu (0) | 2020.02.27 |