Skip to content

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 Texteingabe

Technische 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 Hauptprozess

Projektstruktur

notepad-app/
├── main.js           # Hauptprozess
├── preload.js        # IPC-Brücke
├── package.json
├── index.html        # UI
└── styles.css        # Styling

11.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

  1. IPC-Kommunikation: Renderer sendet Anfragen an Hauptprozess für Dateioperationen
  2. Sicherheit: contextIsolation: true + Preload-Skript
  3. Benutzerfreundlichkeit: Änderungserkennung und Warnung vor dem Schließen
  4. 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
✓ Benachrichtigungsverlauf

11.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
✓ Fehlerbehandlung

11.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.

Frei für alle Anfänger