From cb33c3b5257b2f0751a8f2c75b3f52ab2de922a3 Mon Sep 17 00:00:00 2001 From: JingweiCui Date: Fri, 14 Feb 2025 15:00:10 +0800 Subject: [PATCH] init commit --- CMakeLists.txt | 25 ++++ card_list_widget.cpp | 143 +++++++++++++++++++++ card_list_widget.h | 38 ++++++ card_list_widget.py | 264 ++++++++++++++++++++++++++++++++++++++ checkbox_header_table.cpp | 124 ++++++++++++++++++ checkbox_header_table.h | 41 ++++++ checkbox_header_table.py | 100 +++++++++++++++ 7 files changed, 735 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 card_list_widget.cpp create mode 100644 card_list_widget.h create mode 100644 card_list_widget.py create mode 100644 checkbox_header_table.cpp create mode 100644 checkbox_header_table.h create mode 100644 checkbox_header_table.py diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..239a042 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.16) +project(checkbox_header_table) + +# 添加以下编译选项 +if(MSVC) + add_compile_options(/utf-8) +endif() + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +find_package(Qt6 REQUIRED COMPONENTS Widgets) + +add_executable(checkbox_header_table + checkbox_header_table.h + checkbox_header_table.cpp +) + +target_link_libraries(checkbox_header_table PRIVATE + Qt6::Widgets +) \ No newline at end of file diff --git a/card_list_widget.cpp b/card_list_widget.cpp new file mode 100644 index 0000000..29ab010 --- /dev/null +++ b/card_list_widget.cpp @@ -0,0 +1,143 @@ +#include "card_list_widget.h" +#include +#include +#include + +CardItemDelegate::CardItemDelegate(QObject* parent) + : QStyledItemDelegate(parent) +{ +} + +void CardItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const +{ + painter->save(); + + // 获取数据 + CardData data = index.data(Qt::UserRole).value(); + + // 绘制卡片背景 + QRect rect = option.rect; + rect.adjust(5, 5, -5, -5); // 留出边距 + + // 如果被选中,绘制不同的背景色 + if (option.state & QStyle::State_Selected) { + painter->fillRect(rect, QColor(230, 230, 255)); + } else { + painter->fillRect(rect, Qt::white); + } + + // 绘制卡片边框 + painter->setPen(QPen(Qt::lightGray)); + painter->drawRect(rect); + + // 设置字体 + QFont nameFont = painter->font(); + nameFont.setBold(true); + nameFont.setPointSize(10); + + QFont normalFont = painter->font(); + normalFont.setPointSize(9); + + // 计算文本区域 + int padding = 10; + QRect nameRect = rect.adjusted(padding, padding, -padding, 0); + nameRect.setHeight(25); + + QRect dateRect = nameRect; + dateRect.translate(0, nameRect.height()); + + QRect descRect = dateRect; + descRect.translate(0, dateRect.height()); + descRect.setHeight(40); + + // 绘制文本 + painter->setFont(nameFont); + painter->setPen(Qt::black); + painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter, data.name); + + painter->setFont(normalFont); + painter->setPen(Qt::darkGray); + painter->drawText(dateRect, Qt::AlignLeft | Qt::AlignVCenter, data.date); + + // 绘制描述文本,支持自动换行 + painter->drawText(descRect, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, + data.description); + + painter->restore(); +} + +QSize CardItemDelegate::sizeHint(const QStyleOptionViewItem& option, + const QModelIndex& index) const +{ + Q_UNUSED(option); + Q_UNUSED(index); + return QSize(300, 100); // 卡片固定大小 +} + +MainWindow::MainWindow(QWidget* parent) + : QWidget(parent) +{ + setWindowTitle(tr("卡片列表示例")); + resize(400, 600); + + // 创建布局 + auto layout = new QVBoxLayout(this); + + // 创建列表控件 + listWidget = new QListWidget(this); + listWidget->setSpacing(5); // 设置卡片间距 + listWidget->setViewMode(QListView::ListMode); + listWidget->setItemDelegate(new CardItemDelegate(listWidget)); + + // 设置样式 + listWidget->setStyleSheet( + "QListWidget {" + " background-color: #f0f0f0;" + " border: none;" + "}" + "QListWidget::item {" + " background-color: transparent;" + "}" + "QListWidget::item:selected {" + " background-color: transparent;" + "}" + ); + + layout->addWidget(listWidget); + + // 添加示例数据 + for (int i = 1; i <= 10; ++i) { + CardData data{ + QString(tr("项目 %1")).arg(i), + QDate::currentDate().toString("yyyy-MM-dd"), + QString(tr("这是项目 %1 的详细描述信息,可以包含多行文本内容。")).arg(i) + }; + addCardItem(data); + } +} + +void MainWindow::addCardItem(const CardData& data) +{ + QListWidgetItem* item = new QListWidgetItem(listWidget); + item->setData(Qt::UserRole, QVariant::fromValue(data)); + listWidget->addItem(item); +} + +// 注册自定义数据类型 +Q_DECLARE_METATYPE(CardData) + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + // 设置编码和字体 + QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8")); + QFont font("Microsoft YaHei"); + app.setFont(font); + + MainWindow window; + window.show(); + + return app.exec(); +} \ No newline at end of file diff --git a/card_list_widget.h b/card_list_widget.h new file mode 100644 index 0000000..331a06a --- /dev/null +++ b/card_list_widget.h @@ -0,0 +1,38 @@ +#ifndef CARD_LIST_WIDGET_H +#define CARD_LIST_WIDGET_H + +#include +#include +#include + +// 卡片数据结构 +struct CardData { + QString name; + QString date; + QString description; +}; + +// 自定义委托类来绘制卡片样式 +class CardItemDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + explicit CardItemDelegate(QObject* parent = nullptr); + + void paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const override; + QSize sizeHint(const QStyleOptionViewItem& option, + const QModelIndex& index) const override; +}; + +// 主窗口类 +class MainWindow : public QWidget { + Q_OBJECT +public: + explicit MainWindow(QWidget* parent = nullptr); + +private: + QListWidget* listWidget; + void addCardItem(const CardData& data); +}; + +#endif // CARD_LIST_WIDGET_H \ No newline at end of file diff --git a/card_list_widget.py b/card_list_widget.py new file mode 100644 index 0000000..6f4cdc9 --- /dev/null +++ b/card_list_widget.py @@ -0,0 +1,264 @@ +from PySide6.QtWidgets import (QApplication, QWidget, QListWidget, QListWidgetItem, + QStyledItemDelegate, QVBoxLayout, QStyle, QMenu) +from PySide6.QtCore import Qt, QRect, QSize +from PySide6.QtGui import QPainter, QPen, QColor, QFont, QPainterPath, QLinearGradient +import subprocess +import os +from datetime import date +import sys + +class CardData: + """卡片数据结构""" + def __init__(self, name: str, date: str, description: str): + self.name = name + self.date = date + self.description = description + self.activated = False # 添加激活状态标志 + +class CardItemDelegate(QStyledItemDelegate): + """自定义委托类来绘制卡片样式""" + def paint(self, painter: QPainter, option, index): + painter.setRenderHint(QPainter.Antialiasing) # 启用抗锯齿 + painter.save() + + # 获取数据 + item = index.data(Qt.UserRole) + if not isinstance(item, CardData): + print(f"Warning: Invalid data type: {type(item)}") + return + + # 绘制卡片背景 + rect = option.rect + rect.adjust(8, 4, -8, -4) # 调整边距 + + # 创建圆角路径 + path = QPainterPath() + path.addRoundedRect(rect, 8, 8) + + # 绘制阴影 + shadow_color = QColor(0, 0, 0, 30) + for i in range(5): + shadow_rect = rect.adjusted(0, i, 0, i) + shadow_path = QPainterPath() + shadow_path.addRoundedRect(shadow_rect, 8, 8) + painter.fillPath(shadow_path, shadow_color) + + # 绘制卡片背景 + if item.activated: # 激活状态 + gradient = QLinearGradient(rect.topLeft(), rect.bottomLeft()) + gradient.setColorAt(0, QColor(40, 70, 45)) + gradient.setColorAt(1, QColor(45, 80, 50)) + painter.fillPath(path, gradient) + painter.setPen(QPen(QColor(60, 180, 90), 2)) + elif option.state & QStyle.State_Selected: + gradient = QLinearGradient(rect.topLeft(), rect.bottomLeft()) + gradient.setColorAt(0, QColor(45, 45, 55)) + gradient.setColorAt(1, QColor(55, 55, 65)) + painter.fillPath(path, gradient) + painter.setPen(QPen(QColor(70, 130, 180), 2)) + else: + painter.fillPath(path, QColor(35, 35, 40)) # 深色背景 + painter.setPen(QPen(QColor(60, 60, 65))) + + painter.drawPath(path) + + # 设置字体 + name_font = QFont("Microsoft YaHei", 10) + name_font.setBold(True) + + normal_font = QFont("Microsoft YaHei", 9) + + # 计算文本区域 + padding = 15 + name_rect = rect.adjusted(padding, padding, -padding, 0) + name_rect.setHeight(25) + + date_rect = QRect(name_rect) + date_rect.translate(0, name_rect.height()) + + desc_rect = QRect(date_rect) + desc_rect.translate(0, date_rect.height()) + desc_rect.setHeight(40) + + # 绘制文本 + painter.setFont(name_font) + painter.setPen(Qt.white) # 白色文字 + painter.drawText(name_rect, Qt.AlignLeft | Qt.AlignVCenter, item.name) + + painter.setFont(normal_font) + painter.setPen(QColor(180, 180, 180)) # 浅灰色文字 + painter.drawText(date_rect, Qt.AlignLeft | Qt.AlignVCenter, item.date) + + painter.setPen(QColor(160, 160, 160)) # 描述文字颜色 + painter.drawText(desc_rect, Qt.AlignLeft | Qt.AlignTop | Qt.TextWordWrap, + item.description) + + painter.restore() + + def sizeHint(self, option, index): + return QSize(380, 130) # 略微增加高度 + +class MainWindow(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("卡片列表示例") + self.resize(400, 600) + + # 设置窗口背景色 + self.setStyleSheet("background-color: #1e1e1e;") + + # 创建布局 + layout = QVBoxLayout(self) + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(0) + + # 创建列表控件 + self.list_widget = QListWidget(self) + self.list_widget.setSpacing(10) # 增加卡片间距 + self.list_widget.setResizeMode(QListWidget.Adjust) + self.list_widget.setUniformItemSizes(False) + self.list_widget.setViewMode(QListWidget.ListMode) + self.list_widget.setVerticalScrollMode(QListWidget.ScrollPerPixel) + self.list_widget.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + + # 设置委托 + delegate = CardItemDelegate(self.list_widget) + self.list_widget.setItemDelegate(delegate) + + # 设置样式 + self.list_widget.setStyleSheet(""" + QListWidget { + background-color: #1e1e1e; + border: none; + outline: none; + } + QListWidget::item { + background-color: transparent; + padding: 4px; + } + QListWidget::item:selected { + background-color: transparent; + } + QScrollBar:vertical { + border: none; + background: #1e1e1e; + width: 8px; + margin: 0px; + } + QScrollBar::handle:vertical { + background: #404040; + min-height: 20px; + border-radius: 4px; + } + QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { + height: 0px; + } + QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { + background: none; + } + """) + + layout.addWidget(self.list_widget) + + # 添加示例数据 + for i in range(1, 11): + data = CardData( + name=f"项目 {i}", + date=date.today().strftime("%Y-%m-%d"), + description=f"这是项目 {i} 的详细描述信息,可以包含多行文本内容。这是一个较长的描述,用于测试换行效果。" + ) + self.add_card_item(data) + + # 连接信号 + self.list_widget.itemDoubleClicked.connect(self.on_item_double_clicked) + self.list_widget.customContextMenuRequested.connect(self.show_context_menu) + self.list_widget.setContextMenuPolicy(Qt.CustomContextMenu) + + def add_card_item(self, data: CardData): + """添加卡片项目""" + item = QListWidgetItem(self.list_widget) + item.setData(Qt.UserRole, data) + # 设置item大小 + item.setSizeHint(QSize(380, 120)) + # 设置item可选 + item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable) + + def on_item_double_clicked(self, item): + """双击处理""" + data = item.data(Qt.UserRole) + if isinstance(data, CardData): + data.activated = not data.activated # 切换激活状态 + self.list_widget.viewport().update() # 刷新视图 + + def show_context_menu(self, position): + """显示右键菜单""" + item = self.list_widget.itemAt(position) + if not item: + return + + menu = QMenu(self) + + # 创建动作 + send_action = menu.addAction("发送到设备") + edit_action = menu.addAction("修改") + delete_action = menu.addAction("删除") + show_in_explorer_action = menu.addAction("在资源管理器中显示") + + # 显示菜单并获取选择的动作 + action = menu.exec(self.list_widget.viewport().mapToGlobal(position)) + + if not action: + return + + # 处理菜单动作 + if action == send_action: + self.send_to_device(item) + elif action == edit_action: + self.edit_item(item) + elif action == delete_action: + self.delete_item(item) + elif action == show_in_explorer_action: + self.show_in_explorer(item) + + def send_to_device(self, item): + """发送到设备""" + data = item.data(Qt.UserRole) + print(f"发送到设备: {data.name}") + # TODO: 实现发送到设备的具体逻辑 + + def edit_item(self, item): + """编辑项目""" + data = item.data(Qt.UserRole) + print(f"编辑项目: {data.name}") + # TODO: 实现编辑对话框 + + def delete_item(self, item): + """删除项目""" + row = self.list_widget.row(item) + self.list_widget.takeItem(row) + + def show_in_explorer(self, item): + """在资源管理器中显示""" + data = item.data(Qt.UserRole) + # 这里假设每个项目都有一个关联的文件路径 + # TODO: 需要在CardData中添加文件路径属性 + file_path = os.path.abspath(".") # 示例使用当前目录 + if os.name == 'nt': # Windows + subprocess.run(['explorer', '/select,', file_path]) + elif os.name == 'posix': # macOS 和 Linux + if sys.platform == 'darwin': # macOS + subprocess.run(['open', '-R', file_path]) + else: # Linux + subprocess.run(['xdg-open', os.path.dirname(file_path)]) + +if __name__ == "__main__": + app = QApplication(sys.argv) + + # 设置默认字体 + font = QFont("Microsoft YaHei", 9) + app.setFont(font) + + window = MainWindow() + window.show() + + sys.exit(app.exec()) \ No newline at end of file diff --git a/checkbox_header_table.cpp b/checkbox_header_table.cpp new file mode 100644 index 0000000..2d85dc4 --- /dev/null +++ b/checkbox_header_table.cpp @@ -0,0 +1,124 @@ +#pragma execution_character_set("utf-8") + +#include "checkbox_header_table.h" +#include +#include +#include + +CheckBoxHeader::CheckBoxHeader(Qt::Orientation orientation, QWidget* parent) + : QHeaderView(orientation, parent) + , isChecked(false) +{ + setSectionsClickable(true); + connect(this, &QHeaderView::sectionClicked, this, &CheckBoxHeader::handleSectionClicked); +} + +void CheckBoxHeader::paintSection(QPainter* painter, const QRect& rect, int logicalIndex) const +{ + painter->save(); + + if (logicalIndex == 0) { + const int checkboxSize = 15; + checkboxRect = rect; + + // 计算复选框位置 + int x = rect.x() + 5; // 从左边留出5像素的间距 + int y = rect.y() + (rect.height() - checkboxSize) / 2; + + // 绘制复选框 + painter->setPen(Qt::black); + painter->drawRect(x, y, checkboxSize, checkboxSize); + + if (isChecked) { + painter->drawLine(x + 3, y + 7, x + 6, y + 10); + painter->drawLine(x + 6, y + 10, x + 12, y + 4); + } + + // 绘制文字 + QRect textRect = rect.adjusted(checkboxSize + 10, 0, 0, 0); + painter->drawText(textRect, Qt::AlignVCenter, tr("选择")); + } else { + QHeaderView::paintSection(painter, rect, logicalIndex); + } + + painter->restore(); +} + +void CheckBoxHeader::handleSectionClicked(int logicalIndex) +{ + if (logicalIndex == 0 && !checkboxRect.isNull()) { + isChecked = !isChecked; + emit checkBoxClicked(isChecked); + updateSection(0); + } +} + +MainWindow::MainWindow(QWidget* parent) + : QMainWindow(parent) +{ + setWindowTitle(tr("表头复选框示例")); + resize(400, 300); + + // 创建表格 + table = new QTableWidget(5, 3, this); + setCentralWidget(table); + + // 设置表头 + auto header = new CheckBoxHeader(Qt::Horizontal, table); + table->setHorizontalHeader(header); + connect(header, &CheckBoxHeader::checkBoxClicked, + this, &MainWindow::onHeaderCheckBoxClicked); + + // 设置表头标题 + QStringList headers; + headers << tr("选择") << tr("列1") << tr("列2"); + table->setHorizontalHeaderLabels(headers); + + // 调整列宽 + table->horizontalHeader()->setStretchLastSection(true); + table->horizontalHeader()->resizeSection(0, 80); + + // 调整表格样式 + table->setShowGrid(true); + table->setAlternatingRowColors(true); + + // 填充表格数据 + for (int row = 0; row < 5; ++row) { + // 添加复选框 + auto checkboxItem = new QTableWidgetItem(); + checkboxItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); + checkboxItem->setCheckState(Qt::Unchecked); + table->setItem(row, 0, checkboxItem); + + // 添加其他数据 + table->setItem(row, 1, new QTableWidgetItem( + QString(tr("数据 %1-1")).arg(row + 1))); + table->setItem(row, 2, new QTableWidgetItem( + QString(tr("数据 %1-2")).arg(row + 1))); + } +} + +void MainWindow::onHeaderCheckBoxClicked(bool checked) +{ + for (int row = 0; row < table->rowCount(); ++row) { + if (QTableWidgetItem* item = table->item(row, 0)) { + item->setCheckState(checked ? Qt::Checked : Qt::Unchecked); + } + } +} + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + // 设置编码 + QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8")); + + // 设置默认字体 + QFont font("Microsoft YaHei"); // 使用微软雅黑字体 + app.setFont(font); + + MainWindow window; + window.show(); + return app.exec(); +} \ No newline at end of file diff --git a/checkbox_header_table.h b/checkbox_header_table.h new file mode 100644 index 0000000..0f9e2ec --- /dev/null +++ b/checkbox_header_table.h @@ -0,0 +1,41 @@ +#ifndef CHECKBOX_HEADER_TABLE_H +#define CHECKBOX_HEADER_TABLE_H + +#include +#include +#include + +class CheckBoxHeader : public QHeaderView { + Q_OBJECT + +public: + explicit CheckBoxHeader(Qt::Orientation orientation, QWidget* parent = nullptr); + +signals: + void checkBoxClicked(bool checked); + +protected: + void paintSection(QPainter* painter, const QRect& rect, int logicalIndex) const override; + +private slots: + void handleSectionClicked(int logicalIndex); + +private: + bool isChecked; + mutable QRect checkboxRect; +}; + +class MainWindow : public QMainWindow { + Q_OBJECT + +public: + explicit MainWindow(QWidget* parent = nullptr); + +private slots: + void onHeaderCheckBoxClicked(bool checked); + +private: + QTableWidget* table; +}; + +#endif // CHECKBOX_HEADER_TABLE_H \ No newline at end of file diff --git a/checkbox_header_table.py b/checkbox_header_table.py new file mode 100644 index 0000000..cedf11c --- /dev/null +++ b/checkbox_header_table.py @@ -0,0 +1,100 @@ +from PySide6.QtWidgets import (QApplication, QMainWindow, QTableWidget, + QTableWidgetItem, QHeaderView) +from PySide6.QtCore import Qt, Signal +from PySide6.QtGui import QPainter +import sys + +class CheckBoxHeader(QHeaderView): + checkbox_clicked = Signal(bool) # 创建信号用于传递复选框状态 + + def __init__(self, orientation, parent=None): + super().__init__(orientation, parent) + self.is_checked = False + self.checkbox_rect = None + self.setSectionsClickable(True) + self.sectionClicked.connect(self.handle_section_clicked) + + def paintSection(self, painter: QPainter, rect, logical_index): + painter.save() + + if logical_index == 0: # 在第一列绘制复选框 + checkbox_size = 15 + self.checkbox_rect = rect + + # 计算复选框位置,稍微向左偏移 + x = rect.x() + 5 # 从左边留出5像素的间距 + y = rect.y() + (rect.height() - checkbox_size) // 2 + + # 绘制复选框 + painter.setPen(Qt.black) + painter.drawRect(x, y, checkbox_size, checkbox_size) + + if self.is_checked: + painter.drawLine(x + 3, y + 7, x + 6, y + 10) + painter.drawLine(x + 6, y + 10, x + 12, y + 4) + + # 绘制文字 + text_rect = rect.adjusted(checkbox_size + 10, 0, 0, 0) # 文字位置在复选框右侧 + painter.drawText(text_rect, Qt.AlignVCenter, "选择") + else: + # 其他列正常绘制 + super().paintSection(painter, rect, logical_index) + + painter.restore() + + def handle_section_clicked(self, logical_index): + if logical_index == 0 and self.checkbox_rect: + self.is_checked = not self.is_checked + self.checkbox_clicked.emit(self.is_checked) + self.updateSection(0) + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("表头复选框示例") + self.resize(400, 300) + + # 创建表格 + self.table = QTableWidget(5, 3) + self.setCentralWidget(self.table) + + # 设置表头 + header = CheckBoxHeader(Qt.Horizontal, self.table) + self.table.setHorizontalHeader(header) + header.checkbox_clicked.connect(self.on_header_checkbox_clicked) + + # 设置表头标题 + self.table.setHorizontalHeaderLabels(["选择", "列1", "列2"]) + + # 调整列宽 + self.table.horizontalHeader().setStretchLastSection(True) # 最后一列自动拉伸 + self.table.horizontalHeader().resizeSection(0, 80) # 设置第一列宽度为80像素 + + # 调整表格样式 + self.table.setShowGrid(True) # 显示网格线 + self.table.setAlternatingRowColors(True) # 交替行颜色 + + # 填充表格数据 + for row in range(5): + # 添加复选框 + checkbox_item = QTableWidgetItem() + checkbox_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + checkbox_item.setCheckState(Qt.Unchecked) + self.table.setItem(row, 0, checkbox_item) + + # 添加其他数据 + self.table.setItem(row, 1, QTableWidgetItem(f"数据 {row+1}-1")) + self.table.setItem(row, 2, QTableWidgetItem(f"数据 {row+1}-2")) + + def on_header_checkbox_clicked(self, checked): + # 处理表头复选框点击事件 + for row in range(self.table.rowCount()): + item = self.table.item(row, 0) + if item: + item.setCheckState(Qt.Checked if checked else Qt.Unchecked) + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec()) \ No newline at end of file