Neue Webseite, neues Glück.
Softwareentwickler | Go | Typescript | Java | C++ | R
Neue Webseite, neues Glück.
One plan for this summer is getting my old 1541 back in working condition. Here are a few pictures, because there are only few on the internet.
The capacitors on the mainboard. I haven't replaced them yet. None of these appear to have leaked.
The capacitors on the controller. I couldn't read the voltage on these and the service manual also doesn't specify them.
The old belt vs the new one. The old one doesn't expand at all anymore.
Vor etwas über einem Jahr bestellt ist er diese Woche angekommen.
Der Nitrokey 3C NFCIch hatte ihn damals vorbestellt in der Hoffnung ihn für SSH Schlüssel verwenden zu können. Die meisten Lösungen wie die YubiKeys können das zwar auch,
verwenden aber die kryptographisch zweifelhaften sogenannten NIST-Kurven die ich gerne vermeiden würde.
Die dafür nötigen features waren damals erst geplant
und sind es immer noch. Aber die Firmware Version 1.4.0 steht gerade in den Startlöchern und der Release Candidate kann schon installiert werden.
Die Firmware Version 1.4.0 liefert die OpenPGP Implementierung in dem Hardware Key nach.
Dies soll kein Review sein, dafür habe ich den Key nicht lange genug. Die Hülle sieht aus, als ob man sie im Zweifel leicht aufkriegt.
Wie bruchsicher der USB-C Stecker auf der Platine befestigt ist weiß ich nicht. Er fühlt sich aber fest an. In jedem Fall sollte ein solcher
Hardware Key niemals die einzige Möglichkeit sein, sich irgendwo einzuloggen. Es empfiehlt sich immer einen zweit Schlüssel im Schrank, oder
besser noch, ein zweiter SSH Private Key passwortverschlüsselt auf einer CD im Schrank.
Dies soll einfach nur ein kleines Kompendium sein über die wichtigsten Befehle und Konfigurationen.
Installationsanleitung für das CLI Tool nitropy findet sich auf der Webseite des Herstellers.Sicherstellen, dass gpg installiert ist. Dazu noch pinentry-mac um den Pin per GUI eingeben zu können. Das spart einem ggf. später Überraschungen bei GUI Tools und IDEs die keine PIN Eingabe per TUI anbieten und einfach einfrieren.
brew install gpg pinentry-mac
Ein paar Konfigurationen anpassen:
In `~/.gnupg/gpg.conf` muss `use-agent` enthalten sein.
In `~/.gnugpg/gpg-agent.conf` muss `pinentry-program /usr/local/bin/pinentry-mac` enthalten sein. Den Pfad zu pinentry-mac dabei ggf. anpassen.
In der `~/.zshrc` oder entsprechender Shell konfiguration muss folgendes enthalten sein:
unset SSH_AGENT_PID if [ "${gnupg_SSH_AUTH_SOCK_by:-0}" -ne $$ ]; then export SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" fi
Damit sollte SSH den SSH Schlüssel im Hardware Key zur Authentifizierung verwenden können. Das geht natürlich auch mit allen Programmen die SSH verwenden, wie z.B. git.
Zur Schlüsselverwaltung hat wieder Nitrokey eine gute Anleitung.Das wichtigste:
Wird angezeigt als z.B.: `pub rsa2048/SCHLÜSSELID`
Nitokey schreiben in ihrem Blog, dass derzeit die Privaten Schlüssel noch im normalen Flash Speicher abgelegt werden. Hier können sie wieder ausgelesen werden. Eigentlich sollen die Schlüssel in einem Secure-Element abgelegt werden, wo dies nicht möglich sein soll. Das wird wohl in einem kommenden Update nachgereicht.
Blog von NitrokeyDas macht die Schlüssel nicht unbrauchbar, aber man sollte es im Hinterkopf haben.
Die Arbeit mit Threads ist seit C++11 deutlich einfacher geworden.
Folgendes Beispiel zeigt wie `std::thread` verwendet werden kann:
#include#include #include #include std::vector calcPrimes(int maxNumber) { std::vector primes; for (int c = 2; c <= maxNumber; ++c) { bool prime = [&c]()->bool{ for (int i = 2; i <= sqrt(c); ++i) { if (c % i == 0) { return false; } } return true; }(); if (prime) { primes.push_back(c); } } return primes; } int main() { int maxNumber = 1000000; std::vector primes; auto t = std::thread([&primes](int n)->void { auto calculatedPrimes = calcPrimes(n); primes.insert(primes.end(), calculatedPrimes.begin(), calculatedPrimes.end()); }, maxNumber); // Do other stuff t.join(); std::cout << "Generated " << primes.size() << " primes." << std::endl; }
Aber dieses Beispiel ist nicht threadsicher.
Um es Threadsicher zu machen müssen wir den Vector `primes` vor konkurrierendem Zugriff schützen.
Ein Beispiel dazu sähe folgendermaßen aus:
#include#include #include #include #include class ThreadsafePrimeCalculator { public: void calcPrimes(int maxNumber) { if (t.joinable()) t.join(); t = std::thread([this](int maxNumber) -> void { _calcPrimes(maxNumber); }, maxNumber); } std::vector getPrimes() { if (t.joinable()) t.join(); std::lock_guard guard{m}; std::vector copy = primes; return std::move(copy); } ~ThreadsafePrimeCalculator() { if (t.joinable()) t.join(); } private: void _calcPrimes(int maxNumber) { std::lock_guard guard{m}; primes.clear(); for (int c = 2; c <= maxNumber; ++c) { bool prime = [&c]()->bool{ for (int i = 2; i <= sqrt(c); ++i) { if (c % i == 0) { return false; } } return true; }(); if (prime) { primes.push_back(c); } } } std::thread t; std::vector primes; std::mutex m; }; int main() { int maxNumber = 1000000; ThreadsafePrimeCalculator tspc{}; tspc.calcPrimes(maxNumber); // Do other stuff std::cout << "Generated " << tspc.getPrimes().size() << " primes." << std::endl; }
Aber um uns das zu sparen bietet C++ ein anderes Feature: `std::future`
Futures sind Objekte die in der Zukunft ein Ergebnis zurückliefern. Im Grunde sind es Threads mit Rückgabewert.
Das obere Beispiel lässt sich damit Threadsicher umschreiben ohne Lock Guards und Mutex.
#include#include #include #include #include std::vector calcPrimes(int maxNumber) { std::vector primes; for (int c = 2; c <= maxNumber; ++c) { bool prime = [&c]()->bool{ for (int i = 2; i <= sqrt(c); ++i) { if (c % i == 0) { return false; } } return true; }(); if (prime) { primes.push_back(c); } } return primes; } int main() { int maxNumber = 1000000; auto a = std::async(std::launch::async, calcPrimes, maxNumber); // Do other stuff std::cout << "Generated " << a.get().size() << " primes." << std::endl; }
Edit: Am 3.2.2024 für Kompatibilität mit QT 6.6.1 überarbeitet.
Das Projekt ist nun auch auf Codeberg verfügbar.
https://codeberg.org/lriecken/qt_table_view_widget_tutorial---
Dieses Tutorial beinhaltet folgende Themen:
- Eine Datenmodellklasse wird durch eine `QAbstractTableModel` Klasse mit einem `QTableView` Widget verknüpft.
- Die Tabelle wird editierbar.
- Einträge können hinzugefügt und entfernt werden.
- Für eine Spalte der Tabelle wird ein angepasstes Editorwidget erstellt.
Das Modell besteht aus den Feldern: firstName, lastName, age und gender. Später wird für das letzte Feld eine `ItemDelegate` Klasse angelegt um die Auswahl über eine `QComboBox` zu ermöglichen.
Grundlegende C++ Kenntnisse werden vorausgesetzt.
Unsere Beispiel Modellklasse:
#include <QString> class ContactModel { public: ContactModel(const QString &firstname = "", const QString &lastname = "", int age = 0, const QString &gender = "-"): firstName(firstname), lastName(lastname), age(age), gender(gender) {} QString getFirstName() const { return firstName; } void setFirstName(const QString &value) { firstName = value; } QString getLastName() const { return lastName; } void setLastName(const QString &value) { lastName = value; } int getAge() const { return age; } void setAge(int value) { age = value; } QString getGender() const { return gender; } void setGender(const QString &value) { gender = value; } private: QString firstName; QString lastName; int age; QString gender; };
Das `MainWindow` besteht aus einem `QTableView` Widget und zwei `QButton` Widgets. addButton und removeButton.
Das Fenster wird wie folgt erstellt:
Um in Qt Daten in einem View Widget darzustellen, benötigen wir eine Adapter Klasse, die die Daten oder eine Referenz auf die Daten enthält und in unserem Fall von `QAbstractTableModel` erbt.
Das Interface unserer Klasse ist wie folgt:
#include <QWidget> #include <QVariant> #include <QList> #include <QAbstractTableModel> #include <QSharedPointer> #include "contactmodel.h" class ContactsContainer: public QAbstractTableModel { public: ContactsContainer(QObject *parent); int rowCount(const QModelIndex &parent = QModelIndex()) const override { return entries.size(); } int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 4; } QVariant data(const QModelIndex &index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; Qt::ItemFlags flags(const QModelIndex &index) const override {} bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override {} bool insertRows(int position, int rows, const QModelIndex &parent) override {} bool removeRows(int position, int rows, const QModelIndex &parent) override {} private: QList<QSharedPointer<ContactModel>> entries; };
Neben dem Datensatz `entries`, der in einer `QList` gespeichert wird, enthält die Klasse die Funktionen um die Zeilen- und Spaltenanzahl (`rowCount`, `columnCount`), die Daten der Zellen (referenziert über einen `QModelIndex`) `data` sowie die Spaltenbeschriftung `headerData` abzufragen.
Dies sind alle notwendigen Funktionen, die implementiert werden müssen.
Auf die Funktionen `setData`, `flags`, `insertRows` und `removeRows` kommen wir später zurück.
Beginnen wird damit im Constructor unserer `ContactsContainer` Klasse ein paar Beispieleinträge einzufügen, damit wir sehen ob Daten in der Tabelle angezeigt werden:
ContactsContainer::ContactsContainer(QObject *parent): QAbstractTableModel(parent) { this->entries.push_back( QSharedPointer<ContactModel>::create("John", "Lennon", 25, "Männlich")); this->entries.push_back( QSharedPointer<ContactModel>::create("Paul", "McCartney", 23, "Männlich")); this->entries.push_back( QSharedPointer<ContactModel>::create("George", "Harrison", 22, "Männlich")); this->entries.push_back( QSharedPointer<ContactModel>::create("Ringo", "Starr", 25, "Männlich")); }
Nun implementieren wird die Funktionen `headerData`:
QVariant ContactsContainer::headerData(int section, Qt::Orientation orientation, int role) const { if (role == Qt::DisplayRole) { if (orientation == Qt::Horizontal) { switch(section) { case 0: return "Vorname"; case 1: return "Nachname"; case 2: return "Alter"; case 3: return "Geschlecht"; default: return "unknown"; } } } return QVariant(); }
Wir geben einfach den Namen der Spalte zurück, nach der wir über die Variable `section` gefragt werden.
Zusätzlich prüfen wir über `role` welchem Zweck die Anfrage dient. In unserem Fall `Qt::DisplayRole`. Eine andere Möglichkeit wäre `Qt::EditRole` worauf wir später zurückkommen.
Sehr ähnlich sieht die Funktion `data` aus:
QVariant ContactsContainer::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() > this->entries.size()) { return QVariant(); } if (role == Qt::DisplayRole || role == Qt::EditRole) { switch (index.column()) { case 0: return this->entries.at(index.row())->getFirstName(); case 1: return this->entries.at(index.row())->getLastName(); case 2: return this->entries.at(index.row())->getAge(); case 3: return this->entries.at(index.row())->getGender(); default: return "unknown"; } } return QVariant(); }
Wir prüfen erstmal ob der Index gültig ist und nicht größer als unser Datensatz. Dann geben wir für die Rollen `Qt::DisplayRole` und `Qt::EditRole` den entsprechenden Wert im Datensatz für die entsprechende Spalte zurück.
Jetzt sind wir soweit die Klasse ausprobieren zu können.
Im MainWindow fügen wir den ContactsContainer als Member Variable hinzu. `mainwindow.h` sollte jetzt folgendermaßen aussehen:
#include <QMainWindow> #include "contactscontainer.h" QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); private slots: void on_addButton_clicked(); void on_removeButton_clicked(); private: Ui::MainWindow *ui; ContactsContainer container; };
Im `constructor` initialisieren wir jetzt noch den `container` und verknüpfen ihn mit unserem `tableView` Widget:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , container(this) { ui->setupUi(this); ui->tableView->setModel(&this->container); }
Starten wir das Program, sollten wir folgendes Fenster sehen:
Fügen wir nun etwas Interaktivität hinzu. Dazu fehlen uns zwei Funktionen. `setData` um neue Daten in das Model zu schreiben, und `flags`, um dem TableView Widget zu sagen, welche Felder editierbar sein sollen.
bool ContactsContainer::setData(const QModelIndex &index, const QVariant &value, int role) { if (index.isValid() && role == Qt::EditRole) { switch(index.column()) { case 0: this->entries.at(index.row())->setFirstName(value.toString()); break; case 1: this->entries.at(index.row())->setLastName(value.toString()); break; case 2: this->entries.at(index.row())->setAge(value.toInt()); break; case 3: this->entries.at(index.row())->setGender(value.toString()); break; default: return false; } emit dataChanged(index, index, {role}); return true; } return false; }
Analog zu `data` schreiben wir nun den Wert in das Model. Am Ende lösen wir das `dataChanged` Signal aus um alle Objekte zu informieren, die mit dem Signal verknüpft sind.
Qt::ItemFlags ContactsContainer::flags(const QModelIndex &index) const { if (!index.isValid()) { return Qt::ItemIsEnabled; } return QAbstractItemModel::flags(index) | Qt::ItemIsEditable; }
Durch die Bitweise-Oder Operation setzen wir in dem Flag für alle Felder das `Qt::ItemIsEditable` flag.
Führen wir das Program aus, können wir nun per Doppelklick die Zellen des TableView Widgets bearbeiten.
Qt hält für alle QVariant Typen entsprechende ItemDelegate Klassen vor. Diese Klassen erstellen Widgets, schreiben und lesen ihre Werte, und passieren ihre Geometrie in das Tabellenfeld ein.
Eine solche Klasse zu erstellen und anstelle der vorgehaltenen Klassen zu verwenden ist einfach.
Um zum Beispiel das Feld Geschlecht durch eine QComboBox zu ersetzen müssen wir nur eine eigene Klasse, die von `QStyledItemDelegate` erbt, erstellen.
class GenderOptionsDelegate: public QStyledItemDelegate { Q_OBJECT public: GenderOptionsDelegate(QObject *parent = nullptr): QStyledItemDelegate(parent) { } QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; void setEditorData(QWidget *editor, const QModelIndex &index) const override; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override; };
`createEditor` erstell eine Instanz des gewünschten Widgets. `setEditorData` schreibt Daten aus dem Model in das Widget. `setModelData` liest die Daten aus dem Widget und schreibt sie ins Model. `updateEditorGeometry` setzt die Dimensionen des Widgets anhand der Tabellenzelle.
QWidget *GenderOptionsDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { QComboBox *editor = new QComboBox(parent); editor->setFrame(false); editor->addItem("-"); editor->addItem("Männlich"); editor->addItem("Weiblich"); editor->addItem("Divers"); editor->setAutoFillBackground(true); return editor; }
void GenderOptionsDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { auto value = index.model()->data(index, Qt::EditRole).toString(); static_cast<QComboBox*>(editor)->setCurrentText(value); }
void GenderOptionsDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { auto value = static_cast<QComboBox*>(editor)->currentText(); model->setData(index, value, Qt::EditRole); }
void GenderOptionsDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const { editor->setGeometry(option.rect); }
Jetzt müssen wir nur dem TableView Widget im Constructor unseres MainWindows mitteilen, diese Delegate Klasse zu verwenden:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , container(this) { ui->setupUi(this); ui->tableView->setModel(&this->container); GenderOptionsDelegate *delegate = new GenderOptionsDelegate(); ui->tableView->setItemDelegateForColumn(3, delegate); }
Führen wir nun das Program aus, bekommen wir in der 4. Spalte nun statt einer EditBox eine ComboBox zu sehen.
Im letzten Schritt fügen wir noch die Funktionen `insertRows` und `removeRows` ein.
bool ContactsContainer::insertRows(int position, int rows, const QModelIndex &parent) { this->beginInsertRows(QModelIndex(), position, position + rows - 1); for (int row = 0; row < rows; ++row) { if (position < this->entries.length()) { this->entries.insert(position, QSharedPointer<ContactModel>::create()); } else { this->entries.emplaceBack(QSharedPointer<ContactModel>::create()); } } this->endInsertRows(); return true; }
Die Funktion bekommt die Zeile __vor__ der die neue Zeile eingefügt werden soll. Die Anzahl der Zeilen und einen `QModelIndex` den wir nicht benötigen übergeben.
Als erstes sperren wir das Model durch `beginInsertRows`. Dann fügen wir ein neues ContactModel an entsprechender Stelle in unseren Datensatz ein. Danach entsperren wir das Model wieder, wodurch wiederum alle entsprechende Signal ausgelöst werden, die das TableView Widget über die Änderung im Model informieren.
bool ContactsContainer::removeRows(int position, int rows, const QModelIndex &parent) { if (position < 0 || position + rows > entries.size()) { return false; } this->beginRemoveRows(QModelIndex(), position, position + rows - 1); for (int row = 0; row < rows; ++row) { this->entries.removeAt(position); } this->endRemoveRows(); return true; }
Analog dazu werden hier die Datensätze entfernt.
Jetzt ist es an der Zeit die onClick Handler der `Hinzufügen` und `Entfernen` Buttons zu schreiben:
void MainWindow::on_addButton_clicked() { auto selection = this->ui->tableView->selectionModel(); int position = container.rowCount() + 1; auto indices = selection->selectedIndexes(); if (indices.size() > 0) { auto index = indices.front(); if (index.isValid()) { position = index.row() + 1; } } this->container.insertRows(position, 1, QModelIndex()); }
Wir holen uns ein `QItemSelectionModel` das die selektierten Zellen, Zeilen und Spalten der Table enthält.
Falls etwas selektiert ist, nehmen wir uns das erste Element und dessen Zeile. Ansonsten setzen wir die Position auf das Ende des Datensatzes. Wir fügen jeweils 1 hinzu, da wir die Zeile nicht vor sondern nach der selektierten Zelle einfügen wollen.
void MainWindow::on_removeButton_clicked() { auto selection = this->ui->tableView->selectionModel(); int from = INT_MAX; int to = INT_MIN; auto indices = selection->selectedIndexes(); for (auto &i : indices) { if (i.row() < from) from = i.row(); if (i.row() > to) to = i.row(); } if (from <= to) { this->container.removeRows(from, to - from + 1, QModelIndex()); } }
Analog zu oben holen wir uns ein `QItemSelectionModel`, berechnen diesmal das Minimum und Maximum der selektierten Zeilen und fügen 1 hinzu um die Anzahl der selektierten Zeilen zu berechnen.
Wenn man Klassen in C++ implementiert ist es häufig eine gute Idee sie für die Algorithmen der Standard Template Library
vorzubereiten.
Wir betrachten folgendes Beispiel:
#include#include class SimpleInt { public: SimpleInt() {} SimpleInt(int value): value(value) {} int getValue() const { return value; } void setValue(int value) { this->value = value; } private: int value; }; int main() { std::vector values; values.push_back(5); values.push_back(1); values.push_back(4); values.push_back(2); values.push_back(3); for (auto &value : values) { std::cout << value.getValue() << std::endl; } return 0; }
Natürlich kann man den Vector jederzeit über ein Lambda sortieren.
std::sort(values.begin(), values.end(), [](const SimpleInt &a, const SimpleInt &b)->bool { return a.getValue() < b.getValue(); });
Aber bequemer ist es, wenn die Klasse selbst einen Standard hat, wie sie sortiert wird.
std::sort(values.begin(), values.end());
Dies zu erreichen ist einfach. std::sort, wie die meisten Algorithmen der STL benötigt nur den `<` operator
um die Reihenfolge zweier Objekte zu bestimmen.
bool operator <(const SimpleInt &a, const SimpleInt &b) { return a.getValue() < b.getValue(); }
Etwas schöner wird die Implementierung, wenn man die Funktion in der Klasse als `friend` markiert.
Unser Beispiel sieht jetzt folgendermaßen aus:
class SimpleInt { public: SimpleInt() {} SimpleInt(int value): value(value) {} int getValue() const { return value; } void setValue(int value) { this->value = value; } friend bool operator <(const SimpleInt &a, const SimpleInt &b); private: int value; }; bool operator <(const SimpleInt &a, const SimpleInt &b) { return a.value < b.value; }
`friend` Methoden einer Klasse sind nicht Teil der Klasse, können aber auf ihre privaten Felder zugreifen.
Häufig kommt es vor, das der Zustand von Objekten zu Debugzwecken in Logdateien geschrieben werden muss.
Auch hier reicht es einfach den `<<` operator zu überladen.
std::ostream& operator <<(std::ostream &os, const SimpleInt &a) { os << a.value; return os; }
Und ihn in der Klasse als `friend` zu markieren.
Wo wir vorher noch mit `getValue()` ein Feld des Objektes holen mussten, reicht es jetzt das Objekt selbst in den Outputstream zu schreiben.
std::cout << value << std::endl;
Unser gesamtes Beispiel sieht jetzt folgendermaßen aus:
#include#include #include class SimpleInt { public: SimpleInt() {} SimpleInt(int value): value(value) {} int getValue() const { return value; } void setValue(int value) { this->value = value; } friend bool operator <(const SimpleInt &a, const SimpleInt &b); friend std::ostream& operator <<(std::ostream &os, const SimpleInt &a); private: int value; }; bool operator <(const SimpleInt &a, const SimpleInt &b) { return a.value < b.value; } std::ostream& operator <<(std::ostream &os, const SimpleInt &a) { os << a.value; return os; } int main() { std::vector values; values.push_back(5); values.push_back(1); values.push_back(4); values.push_back(2); values.push_back(3); std::sort(values.begin(), values.end()); for (auto &value : values) { std::cout << value << std::endl; } return 0; }