<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Simple Library Database Manager (SQLite + QSqlTableModel + QSqlQueryModel)]]></title><description><![CDATA[<p dir="auto">Simple Library Database Manager (SQLite + QSqlTableModel + QSqlQueryModel)<br />
Hey everyone,<br />
I'd like to share a small project I built as a learning exercise -- a library (knihovna) database manager using Qt's SQL module with SQLite. It demonstrates switching between QSqlTableModel for direct CRUD operations and QSqlQueryModel for custom SELECT queries, all inside a single QMainWindow.<br />
What it does<br />
The app manages a simple two-table schema: authors and books with a foreign key relationship. The user can:</p>
<p dir="auto">Switch between the autor and kniha tables via radio buttons, with each view backed by a QSqlTableModel using OnFieldChange edit strategy for immediate in-place editing.<br />
Write arbitrary SQL queries in a dockable QTextEdit panel and execute them through a QSqlQueryModel.<br />
Filter any view in real time -- typing in a QLineEdit builds a CAST(column AS TEXT) LIKE '%...%' filter. For the table model this uses setFilter(), for the query model it wraps the original query as a subselect to preserve existing JOINs and WHERE clauses.<br />
Add and delete rows directly.<br />
Optionally bulk-import authors from a CSV-like text file.</p>
<p dir="auto">A few things I found interesting while building this<br />
Filtering on QSqlQueryModel vs QSqlTableModel. QSqlTableModel::setFilter() handles the simple case nicely, but QSqlQueryModel has no built-in filter mechanism. My approach was to store the original SELECT query and wrap it: SELECT * FROM (originalQuery) WHERE filterCondition. This keeps the user's original query intact regardless of its complexity.<br />
CAST for universal LIKE filtering. Using CAST(columnName AS TEXT) means the same filter logic works on both text and numeric columns without having to check types.<br />
Combo box populated from model columns. When the user switches to a custom SELECT view, the filter combo box is rebuilt dynamically using QSqlQueryModel::headerData(), so it always matches whatever columns the query returns.<br />
Tech stack</p>
<p dir="auto">Qt 5/6 (Widgets + SQL modules)<br />
C++17<br />
SQLite via QSQLITE driver<br />
Qt Designer for the UI layout</p>
<p dir="auto">Full source<br />
<a href="http://Databaze.pro" target="_blank" rel="noopener noreferrer nofollow ugc">Databaze.pro</a><br />
iniQT       += core gui sql</p>
<p dir="auto">greaterThan(QT_MAJOR_VERSION, 4): QT += widgets</p>
<p dir="auto">CONFIG += c++17</p>
<p dir="auto">SOURCES += <br />
main.cpp <br />
mainwindow.cpp</p>
<p dir="auto">HEADERS += <br />
mainwindow.h</p>
<p dir="auto">FORMS += <br />
mainwindow.ui</p>
<p dir="auto">qnx: target.path = /tmp/$${TARGET}/bin<br />
else: unix:!android: target.path = /opt/$${TARGET}/bin<br />
!isEmpty(target.path): INSTALLS += target<br />
main.cpp<br />
cpp#include "mainwindow.h"</p>
<p dir="auto">#include &lt;QApplication&gt;</p>
<p dir="auto">int main(int argc, char *argv[])<br />
{<br />
QApplication a(argc, argv);<br />
MainWindow w;<br />
w.show();<br />
return a.exec();<br />
}<br />
mainwindow.h<br />
cpp#ifndef MAINWINDOW_H<br />
#define MAINWINDOW_H</p>
<p dir="auto">#include &lt;QMainWindow&gt;</p>
<p dir="auto">class QSqlTableModel;<br />
class QSqlQueryModel;</p>
<p dir="auto">QT_BEGIN_NAMESPACE<br />
namespace Ui {<br />
class MainWindow;<br />
}<br />
QT_END_NAMESPACE</p>
<p dir="auto">class MainWindow : public QMainWindow<br />
{<br />
Q_OBJECT</p>
<p dir="auto">public:<br />
MainWindow(QWidget *parent = nullptr);<br />
~MainWindow();<br />
void fillDbFromFile();</p>
<p dir="auto">public slots:<br />
void onPridejRadek();<br />
void onSmazRadek();</p>
<pre><code>void onRadioAutor();
void onRadioKniha();
void onRadioSelect();

void onLineEditFiltr();
</code></pre>
<p dir="auto">private:<br />
Ui::MainWindow *ui;<br />
QSqlTableModel *mTableModel;<br />
QSqlQueryModel *mQueryModel;<br />
QString mSelectDotaz;</p>
<pre><code>void openDatabase();
void addLineToDb(QString&amp; line);

void setTabulka(QString name);
</code></pre>
<p dir="auto">};<br />
#endif // MAINWINDOW_H<br />
mainwindow.cpp<br />
cpp#include "mainwindow.h"<br />
#include "ui_mainwindow.h"</p>
<p dir="auto">#include &lt;QFile&gt;<br />
#include &lt;QSqlDatabase&gt;<br />
#include &lt;QSqlTableModel&gt;<br />
#include &lt;QSqlQuery&gt;<br />
#include &lt;QSqlError&gt;</p>
<p dir="auto">MainWindow::MainWindow(QWidget *parent)<br />
: QMainWindow(parent)<br />
, ui(new Ui::MainWindow)<br />
{<br />
ui-&gt;setupUi(this);<br />
openDatabase();</p>
<pre><code>connect(ui-&gt;buttonPridejRadek, &amp;QPushButton::clicked,
        this, &amp;MainWindow::onPridejRadek);
connect(ui-&gt;buttonSmazRadek, &amp;QPushButton::clicked,
        this, &amp;MainWindow::onSmazRadek);

connect(ui-&gt;radioAutor, &amp;QRadioButton::toggled,
        this, &amp;MainWindow::onRadioAutor);
connect(ui-&gt;radioKniha, &amp;QRadioButton::clicked,
        this, &amp;MainWindow::onRadioKniha);
connect(ui-&gt;radioSelect, &amp;QRadioButton::clicked,
        this, &amp;MainWindow::onRadioSelect);

connect(ui-&gt;lineEditFiltr, &amp;QLineEdit::textChanged,
        this, &amp;MainWindow::onLineEditFiltr);
</code></pre>
<p dir="auto">}</p>
<p dir="auto">MainWindow::~MainWindow()<br />
{<br />
delete mTableModel;<br />
delete mQueryModel;<br />
delete ui;<br />
}</p>
<p dir="auto">void MainWindow::openDatabase()<br />
{<br />
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");<br />
db.setDatabaseName("knihovna3.db");<br />
if (db.open()) {<br />
qDebug() &lt;&lt; "uspech";<br />
} else {<br />
qDebug() &lt;&lt; "nepodarilo se otevrit databazi";<br />
}</p>
<pre><code>QSqlQuery dotaz;
dotaz.exec(" CREATE TABLE IF NOT EXISTS autor(\
               id INTEGER UNIQUE PRIMARY KEY AUTOINCREMENT, \
               jmeno  TEXT\
               )");

dotaz.exec("CREATE TABLE IF NOT EXISTS kniha(\
    id INTEGER UNIQUE PRIMARY KEY AUTOINCREMENT, \
    jmeno   TEXT, \
    datumVydani TEXT,\
    casVydani TEXT,\
    autorId INTEGER REFERENCES autor(id)\
    )");

mQueryModel = new QSqlQueryModel;
mTableModel = new QSqlTableModel;
setTabulka("autor");

//  fillDbFromFile();

ui-&gt;radioAutor-&gt;setChecked(true);
</code></pre>
<p dir="auto">}</p>
<p dir="auto">void MainWindow::fillDbFromFile()<br />
{<br />
QFile file("autor.txt");<br />
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {<br />
qDebug() &lt;&lt; "nepodarilo se otevrit soubor";<br />
return;<br />
}</p>
<pre><code>while (!file.atEnd()) {
    QString line = file.readLine();
    addLineToDb(line);
}
</code></pre>
<p dir="auto">}</p>
<p dir="auto">void MainWindow::addLineToDb(QString &amp;line)<br />
{<br />
QList&lt;QString&gt; radek = line.split(",");<br />
int indexRow = mTableModel-&gt;rowCount();<br />
mTableModel-&gt;insertRow(indexRow);<br />
mTableModel-&gt;setData(mTableModel-&gt;index(indexRow, 0), radek[0]);<br />
mTableModel-&gt;setData(mTableModel-&gt;index(indexRow, 1), radek[1]);<br />
mTableModel-&gt;submitAll();<br />
mTableModel-&gt;select();<br />
}</p>
<p dir="auto">void MainWindow::onPridejRadek()<br />
{<br />
mTableModel-&gt;insertRow(mTableModel-&gt;rowCount());<br />
mTableModel-&gt;submitAll();<br />
}</p>
<p dir="auto">void MainWindow::onSmazRadek()<br />
{<br />
mTableModel-&gt;removeRow(ui-&gt;tableView-&gt;currentIndex().row());<br />
mTableModel-&gt;submitAll();<br />
}</p>
<p dir="auto">void MainWindow::onRadioAutor()<br />
{<br />
setTabulka("autor");<br />
ui-&gt;comboFiltr-&gt;clear();<br />
ui-&gt;comboFiltr-&gt;addItem("jmeno");<br />
}</p>
<p dir="auto">void MainWindow::onRadioKniha()<br />
{<br />
setTabulka("kniha");<br />
ui-&gt;comboFiltr-&gt;clear();<br />
ui-&gt;comboFiltr-&gt;addItem("jmeno");<br />
ui-&gt;comboFiltr-&gt;addItem("datumVydani");<br />
ui-&gt;comboFiltr-&gt;addItem("casVydani");<br />
ui-&gt;comboFiltr-&gt;addItem("autorId");<br />
}</p>
<p dir="auto">void MainWindow::onRadioSelect()<br />
{<br />
QString dotaz = ui-&gt;textEdit-&gt;toPlainText();<br />
if (dotaz.isEmpty()) {<br />
dotaz = "SELECT "<br />
"<a href="http://kniha.id" target="_blank" rel="noopener noreferrer nofollow ugc">kniha.id</a> AS kniha_id, "<br />
"kniha.jmeno AS kniha_nazev, "<br />
"kniha.datumVydani, "<br />
"kniha.casVydani, "<br />
"<a href="http://autor.id" target="_blank" rel="noopener noreferrer nofollow ugc">autor.id</a> AS autor_id, "<br />
"autor.jmeno AS autor_meno "<br />
"FROM kniha, autor";<br />
}<br />
mSelectDotaz = dotaz;</p>
<pre><code>mQueryModel-&gt;setQuery(dotaz);
ui-&gt;tableView-&gt;setModel(mQueryModel);
ui-&gt;tableView-&gt;showColumn(0);

if (mQueryModel-&gt;lastError().isValid()) {
    qDebug() &lt;&lt; "Chyba SQL:" &lt;&lt; mQueryModel-&gt;lastError().text();
}

ui-&gt;comboFiltr-&gt;clear();
for (int i = 0; i &lt; mQueryModel-&gt;columnCount(); ++i) {
    QString columnName = mQueryModel-&gt;headerData(i, Qt::Horizontal).toString();
     ui-&gt;comboFiltr-&gt;addItem(columnName);
}
</code></pre>
<p dir="auto">}</p>
<p dir="auto">void MainWindow::onLineEditFiltr()<br />
{<br />
QString filtr = "CAST(" + ui-&gt;comboFiltr-&gt;currentText() + " AS TEXT)"<br />
" LIKE '%" +<br />
ui-&gt;lineEditFiltr-&gt;text() + "%'";<br />
qDebug() &lt;&lt; filtr;</p>
<pre><code>if(ui-&gt;radioSelect-&gt;isChecked()) {
    if (!mSelectDotaz.isEmpty()) {
        QString selectDotazSFiltrom = QString("SELECT * FROM (%1) WHERE %2")
                                .arg(mSelectDotaz)
                                .arg(filtr);

        mQueryModel-&gt;setQuery(selectDotazSFiltrom);

        if (mQueryModel-&gt;lastError().isValid()) {
            qDebug() &lt;&lt; "Chyba SQL:" &lt;&lt; mQueryModel-&gt;lastError().text();
        }
    }
}
else{
    mTableModel-&gt;setFilter(filtr);
    mTableModel-&gt;select();
}
</code></pre>
<p dir="auto">}</p>
<p dir="auto">void MainWindow::setTabulka(QString name)<br />
{<br />
mTableModel-&gt;setTable(name);<br />
mTableModel-&gt;setEditStrategy(QSqlTableModel::OnFieldChange);<br />
mTableModel-&gt;select();</p>
<pre><code>ui-&gt;tableView-&gt;setModel(mTableModel);
ui-&gt;tableView-&gt;hideColumn(0);

ui-&gt;tableView-&gt;update();
</code></pre>
<p dir="auto">}<br />
mainwindow.ui<br />
xml&lt;?xml version="1.0" encoding="UTF-8"?&gt;<br />
&lt;ui version="4.0"&gt;<br />
&lt;class&gt;MainWindow&lt;/class&gt;<br />
&lt;widget class="QMainWindow" name="MainWindow"&gt;<br />
&lt;property name="geometry"&gt;<br />
&lt;rect&gt;<br />
&lt;x&gt;0&lt;/x&gt;<br />
&lt;y&gt;0&lt;/y&gt;<br />
&lt;width&gt;800&lt;/width&gt;<br />
&lt;height&gt;600&lt;/height&gt;<br />
&lt;/rect&gt;<br />
&lt;/property&gt;<br />
&lt;property name="windowTitle"&gt;<br />
&lt;string&gt;MainWindow&lt;/string&gt;<br />
&lt;/property&gt;<br />
&lt;widget class="QWidget" name="centralwidget"&gt;<br />
&lt;layout class="QGridLayout" name="gridLayout_3"&gt;<br />
&lt;item row="0" column="1"&gt;<br />
&lt;widget class="QRadioButton" name="radioKniha"&gt;<br />
&lt;property name="text"&gt;<br />
&lt;string&gt;Kniha&lt;/string&gt;<br />
&lt;/property&gt;<br />
&lt;/widget&gt;<br />
&lt;/item&gt;<br />
&lt;item row="3" column="1"&gt;<br />
&lt;widget class="QPushButton" name="buttonSmazRadek"&gt;<br />
&lt;property name="text"&gt;<br />
&lt;string&gt;Smaz radek&lt;/string&gt;<br />
&lt;/property&gt;<br />
&lt;/widget&gt;<br />
&lt;/item&gt;<br />
&lt;item row="2" column="0" colspan="3"&gt;<br />
&lt;widget class="QTableView" name="tableView"/&gt;<br />
&lt;/item&gt;<br />
&lt;item row="0" column="2"&gt;<br />
&lt;widget class="QRadioButton" name="radioSelect"&gt;<br />
&lt;property name="text"&gt;<br />
&lt;string&gt;Select&lt;/string&gt;<br />
&lt;/property&gt;<br />
&lt;/widget&gt;<br />
&lt;/item&gt;<br />
&lt;item row="0" column="0"&gt;<br />
&lt;widget class="QRadioButton" name="radioAutor"&gt;<br />
&lt;property name="text"&gt;<br />
&lt;string&gt;Autor&lt;/string&gt;<br />
&lt;/property&gt;<br />
&lt;/widget&gt;<br />
&lt;/item&gt;<br />
&lt;item row="3" column="0"&gt;<br />
&lt;widget class="QPushButton" name="buttonPridejRadek"&gt;<br />
&lt;property name="text"&gt;<br />
&lt;string&gt;Pridej radek&lt;/string&gt;<br />
&lt;/property&gt;<br />
&lt;/widget&gt;<br />
&lt;/item&gt;<br />
&lt;item row="1" column="0"&gt;<br />
&lt;widget class="QLabel" name="label"&gt;<br />
&lt;property name="text"&gt;<br />
&lt;string&gt;Filtr&lt;/string&gt;<br />
&lt;/property&gt;<br />
&lt;/widget&gt;<br />
&lt;/item&gt;<br />
&lt;item row="1" column="2"&gt;<br />
&lt;widget class="QComboBox" name="comboFiltr"/&gt;<br />
&lt;/item&gt;<br />
&lt;item row="1" column="1"&gt;<br />
&lt;widget class="QLineEdit" name="lineEditFiltr"/&gt;<br />
&lt;/item&gt;<br />
&lt;/layout&gt;<br />
&lt;/widget&gt;<br />
&lt;widget class="QMenuBar" name="menubar"&gt;<br />
&lt;property name="geometry"&gt;<br />
&lt;rect&gt;<br />
&lt;x&gt;0&lt;/x&gt;<br />
&lt;y&gt;0&lt;/y&gt;<br />
&lt;width&gt;800&lt;/width&gt;<br />
&lt;height&gt;25&lt;/height&gt;<br />
&lt;/rect&gt;<br />
&lt;/property&gt;<br />
&lt;/widget&gt;<br />
&lt;widget class="QStatusBar" name="statusbar"/&gt;<br />
&lt;widget class="QDockWidget" name="dockWidget"&gt;<br />
&lt;attribute name="dockWidgetArea"&gt;<br />
&lt;number&gt;1&lt;/number&gt;<br />
&lt;/attribute&gt;<br />
&lt;widget class="QWidget" name="dockWidgetContents"&gt;<br />
&lt;layout class="QGridLayout" name="gridLayout_2"&gt;<br />
&lt;item row="0" column="0"&gt;<br />
&lt;widget class="QTextEdit" name="textEdit"/&gt;<br />
&lt;/item&gt;<br />
&lt;item row="1" column="0"&gt;<br />
&lt;spacer name="verticalSpacer"&gt;<br />
&lt;property name="orientation"&gt;<br />
&lt;enum&gt;Qt::Orientation::Vertical&lt;/enum&gt;<br />
&lt;/property&gt;<br />
&lt;property name="sizeHint" stdset="0"&gt;<br />
&lt;size&gt;<br />
&lt;width&gt;20&lt;/width&gt;<br />
&lt;height&gt;40&lt;/height&gt;<br />
&lt;/size&gt;<br />
&lt;/property&gt;<br />
&lt;/spacer&gt;<br />
&lt;/item&gt;<br />
&lt;/layout&gt;<br />
&lt;/widget&gt;<br />
&lt;/widget&gt;<br />
&lt;/widget&gt;<br />
&lt;resources/&gt;<br />
&lt;connections/&gt;<br />
&lt;/ui&gt;<br />
What I'd improve next</p>
<p dir="auto">Add input validation and proper error dialogs instead of qDebug() messages.<br />
Parameterize SQL queries to avoid injection in the filter (currently using string concatenation with LIKE).<br />
Add a proper relational view using QSqlRelationalTableModel to show author names instead of raw IDs in the book table.<br />
Persist column widths and dock widget state with QSettings.</p>
<p dir="auto">Feedback welcome -- especially on better patterns for combining QSqlTableModel and QSqlQueryModel in the same view.</p>
]]></description><link>https://forum.qt.io/topic/164559/simple-library-database-manager-sqlite-qsqltablemodel-qsqlquerymodel</link><generator>RSS for Node</generator><lastBuildDate>Tue, 14 Apr 2026 23:45:24 GMT</lastBuildDate><atom:link href="https://forum.qt.io/topic/164559.rss" rel="self" type="application/rss+xml"/><pubDate>Tue, 14 Apr 2026 06:44:23 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[Reply to Simple Library Database Manager (SQLite + QSqlTableModel + QSqlQueryModel) on Tue, 14 Apr 2026 10:32:16 GMT]]></title><description><![CDATA[<p dir="auto"><a class="plugin-mentions-user plugin-mentions-a" href="/user/c918">@<bdi>c918</bdi></a><br />
You have made various posts full of large blobs of code.  If you expect them to be readable/useful to others you really need to put code blocks inside the forum's <strong>Code</strong> tags --- the <code>&lt;/&gt;</code> button or a line of <code>```</code> (3 backticks) above and below a code block.</p>
]]></description><link>https://forum.qt.io/post/837713</link><guid isPermaLink="true">https://forum.qt.io/post/837713</guid><dc:creator><![CDATA[JonB]]></dc:creator><pubDate>Tue, 14 Apr 2026 10:32:16 GMT</pubDate></item><item><title><![CDATA[Reply to Simple Library Database Manager (SQLite + QSqlTableModel + QSqlQueryModel) on Tue, 14 Apr 2026 06:57:11 GMT]]></title><description><![CDATA[<p dir="auto">Update -- v2 changes:<br />
Small iteration on the code above. Three things changed:</p>
<p dir="auto">fillDbFromFile() is now called automatically on database open instead of being commented out. The input file was renamed from autor.txt to in.txt.<br />
addLineToDb() is currently a stub -- it only prints the line to debug output (qDebug() &lt;&lt; line;) instead of inserting into the model. This makes it easier to verify your input file format before wiring up the actual insert logic.<br />
The default SELECT query now has a proper JOIN condition active: WHERE kniha.autorId = <a href="http://autor.id" target="_blank" rel="noopener noreferrer nofollow ugc">autor.id</a>. In v1 this was commented out, which produced a cartesian product of both tables.</p>
<p dir="auto">Updated openDatabase():<br />
cppif (db.open()) {<br />
qDebug() &lt;&lt; "uspech";<br />
fillDbFromFile();  // now called on startup<br />
}<br />
Updated addLineToDb():<br />
cppvoid MainWindow::addLineToDb(QString &amp;line)<br />
{<br />
qDebug() &lt;&lt; line;  // stub -- plug in your insert logic here<br />
}<br />
Updated default query in onRadioSelect():<br />
cppdotaz = "SELECT "<br />
"<a href="http://kniha.id" target="_blank" rel="noopener noreferrer nofollow ugc">kniha.id</a> AS kniha_id, "<br />
"kniha.jmeno AS kniha_nazev, "<br />
"kniha.datumVydani, "<br />
"kniha.casVydani, "<br />
"<a href="http://autor.id" target="_blank" rel="noopener noreferrer nofollow ugc">autor.id</a> AS autor_id, "<br />
"autor.jmeno AS autor_meno "<br />
"FROM kniha, autor "<br />
"WHERE kniha.autorId = <a href="http://autor.id" target="_blank" rel="noopener noreferrer nofollow ugc">autor.id</a>";  // JOIN is now active<br />
Everything else (header, UI, .pro) stays the same.</p>
]]></description><link>https://forum.qt.io/post/837711</link><guid isPermaLink="true">https://forum.qt.io/post/837711</guid><dc:creator><![CDATA[c918]]></dc:creator><pubDate>Tue, 14 Apr 2026 06:57:11 GMT</pubDate></item></channel></rss>