본문으로 바로가기

728x90

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)

4. 화면 UI 구성(Main Window UI)

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";

적용 화면 미리 보기

Dark Layout

 

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
  },
    ......(이하생략)......  

적용 화면 미리 보기

메인 화면

메인 화면

환경설정 화면

환경설정, 일반
환경설정, 라우트 경로 등록

아이콘 목록 모두 보기 화면

아이콘 모두 보기

 

참고자료

 

소스코드

본 포스트 관련 소스코드는 여기에서 다운로드 가능합니다.

 

728x90