Appearance
Kapitel 11: Grundlegende Praxis
In diesem Kapitel bearbeiten wir drei praktische Projekte, um das Gelernte zu festigen.
Praxis 1: Einfacher Desktop-Notizblock
11.1 Anforderungsanalyse
Ein einfacher Texteditor mit folgenden Funktionen:
- Fenster erstellen
- Textbearbeitung
- Lokales Speichern
- Datei öffnen
Benutzeranforderungen
✓ Text eingeben und bearbeiten
✓ Textdatei speichern
✓ Textdatei öffnen
✓ Titel aktualisieren (Dateiname anzeigen)
✓ Kontextmenü für TexteingabeTechnische Architektur
Hauptprozess (main.js):
- Fenster erstellen
- Dateidialoge anzeigen
- Dateioperationen (fs)
Renderer-Prozess (index.html/renderer.js):
- Textarea für Texteingabe
- Buttons für Speichern/Öffnen
- IPC-Aufrufe an HauptprozessProjektstruktur
notepad-app/
├── main.js # Hauptprozess
├── preload.js # IPC-Brücke
├── package.json
├── index.html # UI
└── styles.css # Styling11.2 Kernimplementierung
main.js (Hauptprozess)
javascript
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const fs = require('fs').promises;
const path = require('path');
let mainWin;
let currentFilePath = null;
function createWindow() {
mainWin = new BrowserWindow({
width: 1000,
height: 700,
title: 'Notizblock - Unbenannt',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
});
mainWin.loadFile('index.html');
// Titel aktualisieren, wenn Datei geändert
mainWin.webContents.on('page-title-updated', (event) => {
event.preventDefault();
});
}
// Datei speichern (als neu)
ipcMain.handle('save-file-as', async (event, content) => {
try {
const result = await dialog.showSaveDialog(mainWin, {
title: 'Datei speichern',
defaultPath: 'untitled.txt',
filters: [
{ name: 'Textdateien', extensions: ['txt'] },
{ name: 'Alle Dateien', extensions: ['*'] }
]
});
if (!result.canceled && result.filePath) {
await fs.writeFile(result.filePath, content, 'utf-8');
currentFilePath = result.filePath;
// Titel aktualisieren
const fileName = path.basename(result.filePath);
mainWin.setTitle(`Notizblock - ${fileName}`);
return { success: true, filePath: result.filePath };
}
return { success: false, error: 'Abgebrochen' };
} catch (error) {
return { success: false, error: error.message };
}
});
// Datei speichern (überschreiben)
ipcMain.handle('save-file', async (event, content) => {
// Wenn bereits ein Dateipfad existiert, direkt speichern
if (currentFilePath) {
try {
await fs.writeFile(currentFilePath, content, 'utf-8');
return { success: true, filePath: currentFilePath };
} catch (error) {
return { success: false, error: error.message };
}
} else {
// Sonst "Speichern unter" aufrufen
return await ipcMain.handle('save-file-as', event, content);
}
});
// Datei öffnen
ipcMain.handle('open-file', async (event) => {
try {
const result = await dialog.showOpenDialog(mainWin, {
title: 'Datei öffnen',
properties: ['openFile'],
filters: [
{ name: 'Textdateien', extensions: ['txt'] },
{ name: 'Alle Dateien', extensions: ['*'] }
]
});
if (!result.canceled && result.filePaths.length > 0) {
const filePath = result.filePaths[0];
const content = await fs.readFile(filePath, 'utf-8');
currentFilePath = filePath;
// Titel aktualisieren
const fileName = path.basename(filePath);
mainWin.setTitle(`Notizblock - ${fileName}`);
return { success: true, content, filePath };
}
return { success: false, error: 'Abgebrochen' };
} catch (error) {
return { success: false, error: error.message };
}
});
// Datei neu erstellen
ipcMain.handle('new-file', (event) => {
currentFilePath = null;
mainWin.setTitle('Notizblock - Unbenchn');
return { success: true };
});
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});preload.js (IPC-Brücke)
javascript
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('notepadAPI', {
newFile: () => ipcRenderer.invoke('new-file'),
openFile: () => ipcRenderer.invoke('open-file'),
saveFile: (content) => ipcRenderer.invoke('save-file', content),
saveFileAs: (content) => ipcRenderer.invoke('save-file-as', content)
});index.html (UI)
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Notizblock</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="toolbar">
<button onclick="newFile()">Neu</button>
<button onclick="openFile()">Öffnen</button>
<button onclick="saveFile()">Speichern</button>
<button onclick="saveFileAs()">Speichern unter...</button>
<span id="status">Bereit</span>
</div>
<textarea
id="editor"
placeholder="Hier Text eingeben..."
oninput="markAsModified()"
></textarea>
<script src="renderer.js"></script>
</body>
</html>renderer.js
javascript
// renderer.js
let isModified = false;
// Neue Datei
async function newFile() {
if (isModified) {
const confirmed = confirm('Möchten Sie die Änderungen speichern?');
if (confirmed) {
await saveFile();
}
}
const result = await window.notepadAPI.newFile();
if (result.success) {
document.getElementById('editor').value = '';
isModified = false;
document.getElementById('status').textContent = 'Neue Datei erstellt';
}
}
// Datei öffnen
async function openFile() {
if (isModified) {
const confirmed = confirm('Möchten Sie die Änderungen speichern?');
if (confirmed) {
await saveFile();
}
}
const result = await window.notepadAPI.openFile();
if (result.success) {
document.getElementById('editor').value = result.content;
isModified = false;
document.getElementById('status').textContent =
`Datei geöffnet: ${result.filePath}`;
}
}
// Datei speichern
async function saveFile() {
const content = document.getElementById('editor').value;
const result = await window.notepadAPI.saveFile(content);
if (result.success) {
isModified = false;
document.getElementById('status').textContent =
`Gespeichert: ${result.filePath}`;
} else if (result.error !== 'Abgebrochen') {
alert('Fehler beim Speichern: ' + result.error);
}
}
// Datei speichern unter
async function saveFileAs() {
const content = document.getElementById('editor').value;
const result = await window.notepadAPI.saveFileAs(content);
if (result.success) {
isModified = false;
document.getElementById('status').textContent =
`Gespeichert: ${result.filePath}`;
} else if (result.error !== 'Abgebrochen') {
alert('Fehler beim Speichern: ' + result.error);
}
}
// Als geändert markieren
function markAsModified() {
if (!isModified) {
isModified = true;
document.getElementById('status').textContent = 'Geändert';
}
}
// Vor dem Schließen warnen
window.addEventListener('beforeunload', (event) => {
if (isModified) {
event.returnValue = 'Möchten Sie die Änderungen speichern?';
}
});styles.css
css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
height: 100vh;
}
.toolbar {
background: #f0f0f0;
padding: 10px;
border-bottom: 1px solid #ccc;
display: flex;
align-items: center;
gap: 10px;
}
.toolbar button {
padding: 8px 16px;
cursor: pointer;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
}
.toolbar button:hover {
background: #e0e0e0;
}
#status {
margin-left: auto;
font-size: 12px;
color: #666;
}
#editor {
flex: 1;
padding: 20px;
font-family: 'Courier New', monospace;
font-size: 14px;
border: none;
outline: none;
resize: none;
}11.3 Code-Erklärung und Optimierung
Wichtige Punkte
- IPC-Kommunikation: Renderer sendet Anfragen an Hauptprozess für Dateioperationen
- Sicherheit:
contextIsolation: true+ Preload-Skript - Benutzerfreundlichkeit: Änderungserkennung und Warnung vor dem Schließen
- Titel-Aktualisierung: Dateiname wird in der Titelleiste angezeigt
Optimierungen
javascript
// Optimierung: Automatisches Speichern (Alle 5 Minuten)
let autoSaveInterval;
function startAutoSave() {
autoSaveInterval = setInterval(async () => {
if (isModified && currentFilePath) {
await saveFile();
}
}, 5 * 60 * 1000); // 5 Minuten
}
// Optimierung: Tastaturkurzel
document.addEventListener('keydown', (event) => {
if (event.ctrlKey || event.metaKey) {
switch(event.key) {
case 'n':
event.preventDefault();
newFile();
break;
case 'o':
event.preventDefault();
openFile();
break;
case 's':
event.preventDefault();
if (event.shiftKey) {
saveFileAs();
} else {
saveFile();
}
break;
}
}
});Praxis 2: Systembenachrichtigungs-Tool
11.4 Anforderungsanalyse
Ein Tool zum Senden geplanter Systembenachrichtigungen.
Benutzeranforderungen
✓ Benachrichtigungstitel und -inhalt eingeben
✓ Zeit für Benachrichtigung festlegen
✓ Benutzerdefiniertes Icon
✓ Wiederkehrende Benachrichtigungen
✓ Benachrichtigungsverlauf11.5 Kernimplementierung
main.js (Teilauszug)
javascript
const { app, BrowserWindow, ipcMain, Notification } = require('electron');
const path = require('path');
let mainWin;
const activeTimers = new Map(); // Timer speichern
function createWindow() {
mainWin = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
});
mainWin.loadFile('index.html');
}
// Benachrichtigung senden
ipcMain.handle('send-notification', async (event, { title, body, icon }) => {
const notification = new Notification({
title: title || 'Benachrichtigung',
body: body || '',
icon: icon || undefined,
silent: false
});
notification.on('click', () => {
if (mainWin) {
mainWin.show();
mainWin.focus();
}
});
notification.show();
return { success: true };
});
// Geplante Benachrichtigung
ipcMain.handle('schedule-notification', async (event, { title, body, delay }) => {
const id = Date.now().toString();
const timer = setTimeout(() => {
const notification = new Notification({
title,
body,
silent: false
});
notification.on('click', () => {
if (mainWin) {
mainWin.show();
mainWin.focus();
}
});
notification.show();
activeTimers.delete(id);
// Renderer benachrichtigen
mainWin.webContents.send('notification-triggered', { id, title });
}, delay);
activeTimers.set(id, timer);
return { success: true, id };
});
// Geplante Benachrichtigung abbrechen
ipcMain.handle('cancel-notification', async (event, id) => {
if (activeTimers.has(id)) {
clearTimeout(activeTimers.get(id));
activeTimers.delete(id);
return { success: true };
}
return { success: false, error: 'ID nicht gefunden' };
});
app.whenReady().then(createWindow);index.html (UI)
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Benachrichtigungstool</title>
<style>
body { font-family: Arial; padding: 20px; max-width: 600px; margin: 0 auto; }
.form-group { margin: 15px 0; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input, textarea { width: 100%; padding: 8px; }
textarea { height: 100px; }
button { padding: 10px 20px; margin: 5px; cursor: pointer; }
#history { margin-top: 20px; }
.history-item { padding: 10px; background: #f0f0f0; margin: 5px 0; border-radius: 5px; }
</style>
</head>
<body>
<h1>Benachrichtigungstool</h1>
<div class="form-group">
<label>Titel:</label>
<input type="text" id="title" placeholder="Benachrichtigungstitel">
</div>
<div class="form-group">
<label>Inhalt:</label>
<textarea id="body" placeholder="Benachrichtigungsinhalt"></textarea>
</div>
<div class="form-group">
<label>Verzögerung (Sekunden):</label>
<input type="number" id="delay" value="5" min="1">
</div>
<div>
<button onclick="sendNow()">Sofort senden</button>
<button onclick="schedule()">Geplant senden</button>
</div>
<div id="history">
<h3>Benachrichtigungsverlauf</h3>
<div id="historyList"></div>
</div>
<script src="renderer.js"></script>
</body>
</html>renderer.js
javascript
// renderer.js
const history = [];
async function sendNow() {
const title = document.getElementById('title').value;
const body = document.getElementById('body').value;
if (!title && !body) {
alert('Bitte Titel oder Inhalt eingeben');
return;
}
const result = await window.notificationAPI.sendNotification({ title, body });
if (result.success) {
addToHistory(title, body, 'Sofort');
}
}
async function schedule() {
const title = document.getElementById('title').value;
const body = document.getElementById('body').value;
const delay = parseInt(document.getElementById('delay').value) * 1000;
if (!title && !body) {
alert('Bitte Titel oder Inhalt eingeben');
return;
}
const result = await window.notificationAPI.scheduleNotification({
title,
body,
delay
});
if (result.success) {
addToHistory(title, body, `Geplant (${delay / 1000}s)`);
alert(`Benachrichtigung in ${delay / 1000} Sekunden gesendet`);
}
}
function addToHistory(title, body, type) {
const item = {
title,
body,
type,
time: new Date().toLocaleTimeString()
};
history.unshift(item);
updateHistoryDisplay();
}
function updateHistoryDisplay() {
const container = document.getElementById('historyList');
container.innerHTML = '';
history.slice(0, 10).forEach(item => {
const div = document.createElement('div');
div.className = 'history-item';
div.innerHTML = `
<strong>${item.title || '(Kein Titel)'}</strong><br>
<small>${item.body || '(Kein Inhalt)'}</small><br>
<small>Typ: ${item.type} | Zeit: ${item.time}</small>
`;
container.appendChild(div);
});
}
// Benachrichtigung wurde ausgelöst
window.notificationAPI.onNotificationTriggered((event, data) => {
alert(`Benachrichtigung ausgelöst: ${data.title}`);
});11.6 Praxis: Zeitgeplante Benachrichtigung und Klick-Aktion
Vollständiges Beispiel siehe oben.
Praxis 3: Netzwerkanfrage-Tool
11.7 Anforderungsanalyse
Ein Tool zum Senden von HTTP-Anfragen und Anzeigen der Antwort.
Benutzeranforderungen
✓ GET/POST-Anfragen senden
✓ Antwortdaten anzeigen (formatiert)
✓ Anfrageverlauf speichern (localStorage)
✓ Ladezeit anzeigen
✓ Fehlerbehandlung11.8 Kernimplementierung
main.js (Hauptprozess - IPC Handler)
javascript
const { ipcMain } = require('electron');
const axios = require('axios');
// HTTP-Anfrage durchführen
ipcMain.handle('http-request', async (event, {
method,
url,
headers,
body
}) => {
try {
const startTime = Date.now();
const response = await axios({
method: method || 'GET',
url,
headers: headers || {},
data: body || undefined,
timeout: 30000, // 30 Sekunden Timeout
validateStatus: () => true // Alle Status-Codes akzeptieren
});
const endTime = Date.now();
const duration = endTime - startTime;
return {
success: true,
status: response.status,
statusText: response.statusText,
headers: response.headers,
data: response.data,
duration
};
} catch (error) {
return {
success: false,
error: error.message,
code: error.code
};
}
});index.html (UI)
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Netzwerkanfrage-Tool</title>
<style>
body { font-family: Arial; padding: 20px; max-width: 1200px; margin: 0 auto; }
.request-form { background: #f0f0f0; padding: 20px; border-radius: 5px; }
.form-row { display: flex; gap: 10px; margin: 10px 0; }
select, input, textarea { flex: 1; padding: 8px; }
textarea { min-height: 100px; font-family: monospace; }
button { padding: 10px 20px; cursor: pointer; background: #4CAF50; color: white; border: none; border-radius: 4px; }
button:hover { background: #45a049; }
#response { margin-top: 20px; }
.response-meta { background: #e0e0e0; padding: 10px; border-radius: 5px; margin-bottom: 10px; }
.response-data { background: #f5f5f5; padding: 10px; border-radius: 5px; max-height: 400px; overflow-y: auto; }
pre { white-space: pre-wrap; word-wrap: break-word; }
.history { margin-top: 20px; }
.history-item { padding: 10px; background: #f0f0f0; margin: 5px 0; cursor: pointer; border-radius: 5px; }
.history-item:hover { background: #e0e0e0; }
</style>
</head>
<body>
<h1>Netzwerkanfrage-Tool</h1>
<div class="request-form">
<div class="form-row">
<select id="method">
<option>GET</option>
<option>POST</option>
<option>PUT</option>
<option>DELETE</option>
<option>PATCH</option>
</select>
<input type="text" id="url" placeholder="https://api.example.com/endpoint" style="flex: 3;">
<button onclick="sendRequest()">Senden</button>
</div>
<div class="form-row">
<textarea id="headers" placeholder="Headers (JSON-Format): { 'Content-Type': 'application/json' }"></textarea>
</div>
<div class="form-row">
<textarea id="body" placeholder="Request Body (JSON-Format)"></textarea>
</div>
</div>
<div id="response" style="display: none;">
<h3>Antwort</h3>
<div class="response-meta" id="responseMeta"></div>
<div class="response-data">
<pre id="responseData"></pre>
</div>
</div>
<div class="history">
<h3>Anfrageverlauf</h3>
<div id="historyList"></div>
<button onclick="clearHistory()">Verlauf löschen</button>
</div>
<script src="renderer.js"></script>
</body>
</html>renderer.js
javascript
// renderer.js
const HISTORY_KEY = 'httpRequestHistory';
// Anfrage senden
async function sendRequest() {
const method = document.getElementById('method').value;
const url = document.getElementById('url').value;
const headersText = document.getElementById('headers').value;
const bodyText = document.getElementById('body').value;
if (!url) {
alert('Bitte URL eingeben');
return;
}
let headers = {};
let body = null;
// Header parsen
if (headersText.trim()) {
try {
headers = JSON.parse(headersText);
} catch (error) {
alert('Fehler beim Parsen der Header: ' + error.message);
return;
}
}
// Body parsen
if (bodyText.trim() && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
try {
body = JSON.parse(bodyText);
} catch (error) {
// Body als Text senden
body = bodyText;
}
}
// Anfrage senden
try {
const result = await window.httpAPI.sendRequest({ method, url, headers, body });
// Antwort anzeigen
displayResponse(result);
// Zum Verlauf hinzufügen
addToHistory({ method, url, result });
} catch (error) {
alert('Fehler: ' + error.message);
}
}
// Antwort anzeigen
function displayResponse(result) {
const responseDiv = document.getElementById('response');
const metaDiv = document.getElementById('responseMeta');
const dataPre = document.getElementById('responseData');
responseDiv.style.display = 'block';
if (result.success) {
metaDiv.innerHTML = `
<strong>Status:</strong> ${result.status} ${result.statusText}<br>
<strong>Dauer:</strong> ${result.duration}ms<br>
<strong>Größe:</strong> ${JSON.stringify(result.data).length} Bytes
`;
dataPre.textContent = JSON.stringify(result.data, null, 2);
} else {
metaDiv.innerHTML = `
<strong>Fehler:</strong> ${result.error}<br>
<strong>Code:</strong> ${result.code || 'N/A'}
`;
dataPre.textContent = 'Anfrage fehlgeschlagen';
}
}
// Zum Verlauf hinzufügen
function addToHistory(entry) {
let history = JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]');
history.unshift({
...entry,
timestamp: new Date().toISOString()
});
// Nur die letzten 20 Einträge speichern
if (history.length > 20) {
history = history.slice(0, 20);
}
localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
updateHistoryDisplay();
}
// Verlauf anzeigen
function updateHistoryDisplay() {
const history = JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]');
const container = document.getElementById('historyList');
container.innerHTML = '';
history.forEach((entry, index) => {
const div = document.createElement('div');
div.className = 'history-item';
div.innerHTML = `
<strong>${entry.method}</strong> ${entry.url}<br>
<small>${new Date(entry.timestamp).toLocaleString()}</small>
${entry.result.success ?
`<span style="color: green;"> ${entry.result.status}</span>` :
`<span style="color: red;"> Fehler</span>`}
`;
div.onclick = () => {
document.getElementById('method').value = entry.method;
document.getElementById('url').value = entry.url;
};
container.appendChild(div);
});
}
// Verlauf löschen
function clearHistory() {
if (confirm('Möchten Sie den gesamten Verlauf löschen?')) {
localStorage.removeItem(HISTORY_KEY);
updateHistoryDisplay();
}
}
// Beim Laden
document.addEventListener('DOMContentLoaded', () => {
updateHistoryDisplay();
});11.9 Praxis: Anfrage senden, Daten anzeigen, Verlauf speichern
Vollständiges Beispiel siehe oben.
Zusammenfassung
In diesem Kapitel haben Sie gelernt:
- Einen einfachen Notizblock zu erstellen (Fenster, IPC, Dateioperationen)
- Ein Systembenachrichtigungs-Tool zu implementieren (Notification, Timer)
- Ein Netzwerkanfrage-Tool zu bauen (HTTP-Requests, Verlauf)
Im nächsten Kapitel werden wir fortgeschrittene Praxisprojekte behandeln.
