По просьбам хабраюзеров, в программе «Luman Box» для Windows реализовано таки масштабирование для работы на HiDPI мониторах. К сожалению, неидеально, ибо у Delphi 10 выявился целый ряд граблей, на которые пока не наступишь, не прочувствуешь :-(
Чтобы включить масштабирование, необходимо выбрать п. меню "Сервис/Настройки системы" перейти на вкладку "Интерфейс", и в ней ввести поле "Коэффициент увеличения экрана (от 100)". Можно, например, указать 150. После перезагрузки программы окна выглядят следующим образом на разрешении 1920*1080 (см. выше).
Реализовал все с помощью процедуры ScaleBy. К сожалению, есть косяки, которые мне так и не удалось решить:
высота строки главного меню неизменна, несмотря на увеличение шрифта. Что только не перепробовал, не работает.
заголовок окон вообще не меняется
не меняется текст служебных сообщений в MessageBox
масштабирование компонента TRichEdit работает только для новых статей. Т.к. старый размер шрифта после добавления фиксируется намертво. И чтобы его менять по коэффициенту, необходимо парсить RTF, а как это полноценно и на лету делать, не знаю.
Однако, пользоваться программой на HiDPI мониторах уже можно. По крайней мере, чтобы потестировать и уловить суть.
«Luman Box» для Windows - Open Source продукт
Также решил потихоньку делать проект Open Source. Сделал репозиторий - https://github.com/korvintaG/lbase . Приглашаю в обсуждение кода - конструктивная критика приветствуется! Интересным мне представляется реализация модулей в папке /Obj - пришлось подумать, но сделал нормально на объектах универсальный двупанельный интерфейс (far наше все) для любых сущностей. Возможно, кому пригодится для сторонних продуктов. Ниже приведены объявления типа двупанельного интерфейса, от которого в дальнейшем делаются наследники (ключевые слова, проекты, источники, содержимое проектов/источников):
Объявление типа двупанельного менеджера на Delphi
type
TPanelTagInfo=class
class_id, class_id_a:longint; // классификаторы панелей текущей и противоположной
gbPanel, gbPanel_a:TGroupBox; // GroupBox панели текущей и противоположной
dbg, dbg_a:TDBGridEh; // гриды панелей текущей и противоположной
pnCont, pnCont_a:TPanel; // состав панелей (об источнике, заметки, ....) текущей и противоположной
class_full_name, class_full_name_a:string; // полные пути классификаторов
class_name, class_name_a:string; // имена классификаторов
dsu, dsu_a:TdataSet; // dataSet панелей текущей и противоположной
tag_name, tag_name_a:string; // 'Left' или 'Right'
end;
// 2-х панельный навигатор по классификаторам
TfmTwoPanelControl=class(TAliasParamForm)
private
// контролы и DataSet, что обязательно должны быть означены дочерними объектами
m_class_left_id, m_class_right_id:integer; // ID папок левой и правой
m_pm_main:TPopUpMenu; // основной popUpMenu
m_dsu_left, m_dsu_right:TdataSet; // DataSet
m_gb_left_panel, m_gb_right_panel:TGroupBox; // GroupBox - основные панели
m_dbg_left, m_dbg_right:TDBGridEh; // гриды
m_pn_cont_left, m_pn_cont_right:TPanel; // панели состава (об источнике, заметки и т.д.)
m_last_class_id:longint; // последний активный классификатор
// главные параметры
m_view_mode:integer; // =0 - слева каталог, справа инфо, =1-справа каталог, слева инфо, =3 - оба каталога
m_view_maximized:boolean; // максимально раскрытый одиночной панели режим?
// вспомогательные переменные
m_panel_root_caption:string; // название коревой панели, обычно константа
m_last_scrool_tag:array[0..1] of longint; // для ускорения - чтобы не перерисовывать дважды-трижды одно и то же
procedure init; // инициализация работы с 2-панельным коммандером
function get_alias_def_class(tag_:integer):string; // получить alias для сохранения значений по умолчанию для панели
function get_alias_nav(tag_:integer):string; // получить alias для навигации
function get_def_class(tag_:integer):longint; // получить папку по умолчанию для панели
function get_last_scrool_tag(tag_:integer):integer; // получить последний обработанный в scrool ID по тагу
function get_sub_class(tag_:integer;class_id_:longint):longint; // получить подкласс для ID папки панели
procedure actualize_pm_main_check; // установить check popapmenu в зависимости от режима отображения
procedure dbg_main_cell_click(Column: TColumnEh); // действие по клику мышкой на ячейке в основном гриде
procedure dbg_main_key_down(Sender: TObject; var Key: Word; Shift: TShiftState); // действие по нажатию клавиши в основном гриде
procedure menu_action_change_panels(Sender: TObject); // поменять панели друг с другом местами
procedure menu_action_choose_the_same(Sender: TObject); // выбрать на другой панели тот же путь
procedure menu_action_set_mode_maximized(Sender: TObject); // режим выставить 1 панель на все окно
procedure menu_action_set_mode_show_content(Sender: TObject); // режим выставить с просмотром содержимого
procedure menu_action_set_mode_2_panel(Sender: TObject); // режим выставить 2-панельный
procedure mode_view_recover; // восстановить режим отображения
procedure mode_view_save; // сохранить режим отображения
procedure set_def_class(tag_:integer;class_id_:longint); // установить ID папки по умолчанию для стороны конкретной панели
procedure set_panel_class_caption(tag_:integer); // выставить заголовок панели
procedure set_last_scrool_tag(tag_:integer;value_:longint);
procedure class_enter(tag_:integer;class_id_, subclass_id_:longint); // вход в папку
procedure class_exit(tag_:integer;old_class_id_, new_class_id_:longint); // выход из папки
public
IgnoreDSUScroolEvent:boolean;
constructor CreateWithAlias(AOwner_: TComponent; alias_,PanelRootCaption_:string);
procedure AssignControls(pmMain_:TPopupMenu; // выставить соответствие основных контролов, нельзя в конструкторе формы, т.к. на том этапе их нет
gbLeftPanel_, gbRightPanel_:TGroupBox;
dbgLeft_, dbgRight_:TDBGridEh;
pnContLeft_, pnContRight_:TPanel;
dsuLeft_, dsuRight_:TdataSet);
function GetTagPTI(tag_:integer):TPanelTagInfo;overload; virtual; // получить PTI в заисимости от tag_
function GetAnotherTag(tag_:integer):integer; // получить ID противоложной панели
function GetCurClass:longint; // получить текущий классификатор
function GetFocusTag:integer; // получить ID текущей панели
procedure ClassUp(Sender_: TObject); // нажим папки '...' - вверх
procedure DsuAfterScroll(DataSet_: TDataSet); virtual; // действие после скрула основного dataset
procedure GetTagPTI(tag_:integer; PTI_:TPanelTagInfo;is_fast_:boolean=false); overload; virtual; // означить PTI в зависимости от tag_
procedure ResetLastScroolTag; // сброс залипания автоскрула
procedure SetAlias(alias_:string); // выставить новый alias (нужно при означивания ID в некоторых формах)
procedure SetPanelClass(tag_:integer;class_id_:integer); overload; // выставить для панели ID папки и совершить нужные действия
procedure SetPanelClass(tag_:integer); overload; // совершить нужные действия при выставления панели
procedure SetViewMode(mode_:integer;maximized_:boolean); overload; virtual; // установить режим отображения
procedure SetViewMode(); overload; virtual; // актуализировать режим отображения
// блок пустых методов для обязательного (!!!!!) определения в дочерних компонентах
function GetClassClassId(class_id_:longint):longint; virtual; abstract; // получить ID папки по ID подпапки
function GetClassNameFull(class_id_:longint):string; virtual; abstract; // получить полный путь по ID
function GetClassName(class_id_:longint):string; virtual; abstract; // получить название классиифкатора по ID
procedure DbgDblClick(Sender_: TObject); virtual; abstract; // двойной щелчок мышью по основному гриду
procedure DsuAfterScroolHandle(DataSet_:TDataSet); virtual; abstract; // реальные действия при скрулинге основного датасет
procedure FormResize(Sender_: TObject);virtual; abstract; // действие при смене размера формы
procedure SetPanelClassAddAction(tag_:integer); virtual; abstract; // дополнительные действия по выбору нового классификатора
published
property ViewMaximized: boolean read m_view_maximized; // режим расширения панели на все окно?
property ViewMode: Integer read m_view_mode; // режим отображения
property ClassLeftId: Integer read m_class_left_id; // левый class_id
property ClassRightId: Integer read m_class_right_id; // правый class_id
end;
Осмысления опыта
Поскольку очень активно пользуюсь своей индивидуальной базой знаний уже более года (в т.ч. при повышении квалификации на on-line курсах), то смог сформулировать необходимый минимум требований. Без которых, на мой взгляд, я бы не стал использовать базу знаний в повседневной жизни. Итак:
Контент заметкоделания
Суть ZettlKasten и индивидуальных баз знаний заключается именно в возможности быстрого погружения в тематический контекст. Но сам процесс делания заметок (работы с базой знаний) тоже явялется в некотором смысле контекстом. И возможность быстрого в него погружения очень важна. К примеру, как я быстро погружаюсь в контекст заметкоделания в программе "Luman Box" для Windows:
сразу после запуска программы обычно нажимаю горячую клавишу "F8" и получаю список тех источников/проектов, с которыми работал последними. Грубо говоря мою историю работы:
Вроде бы мелочь, но за счет целого ряда подобных мелочей погружение в контекст работы с индивидуальной базой знаний происходит мгновенно.
помимо истории списка последних 10 источников/проектов, при выборе конкретного источника сохраняется раздел содержания, где происходила работа. Например, вот так сразу же открываются заметки по книге Толле Экхарта:
В купе с тем, что и fb2 книга открывается на последнем прочитанном месте, то погружение в контекст реально происходит мгновенно. Поэтому работать с базой знаний становится приятно. Не надо делать над собой усилие, чтобы заставить себя делать заметки.
Также погружению в контекст заметкоделания сильно помогает общий список заметок, который можно сортировать как угодно. Например, все заметки как единый список можно отсортировать по дате-времени созданию по убыванию, и увидеть самые свежие.
Также в списке заметок иконками отображается наличие в них внешних WWW-ссылок и приатаченных файлов, что здорово помогает в ориентации по списку. Например, а где там последняя заметка с аттачем и ссылкой?
Кусок исходника
И по прежнему основным недостатком классического ZettelKasten я считаю полный отказ от копипасты оригинала источника. Не, я понимаю, есть люди с уникальным складом ума. Которым, например, обладал Никлас Луман. Но я - обычный программист с российской глубинки. И сразу понял, что в моем случае оригинальный ZettelKasten не сработает. Я уже и ранее пытался делать заметки, а потом, читая их спустя пару месяцев, удивлялся, что за чертовщина мне приходила в голову? Потому сейчас большинство моих заметок выглядит следующим образом:
Обратите внимание, текст справа - это якобы то, что должно быть достаточно в классической базе знаний. На мой взгляд, банальщина и бессмыслица. Но вот вместе с оригиналом (текст слева) это уже воссоздает пережитое понимание контекста. Собственно то, ради чего все это и затевается.
Keyword Commander
Я только начал вести свою индивидуальную базу знаний, а список ключевых слов быстро достиг размера более 200 и стал для разума необозрим. Потому потребовалась иерархизировать ключевые слова. Такой функционал есть во многих индивидуальных базах знаний. Но когда список ключевых слов стал более 500, то иерархизация стала требоваться иной. Нужен был механизм управления этой иерархией. И лучше всего здесь подошел интерфейс классического двупанельного файлового менеджера. Вот как выглядит обычная работа с двупанельным Keyword Commander:
Особенность в том, что иерархизация ключевых слов - это непрекращающийся регулярный процесс. Который напрямую связан с ростом нейронных связей в Вашем мозге. И для управления этим процессом нужен хороший инструмент. Keyword Commander - то, что надо!
Сияющие горизонты
Несмотря на тот профит, который регулярно получаю от работы с индивиудальной базой знаний, я нутром чую, что самое интересное еще впереди. Наше сознание в своей целостности работает как связка ключевых слов и контекста. Из-за всем известных ограничений нашего мозга (память, внимание, самодисциплина) осознание человека не может проявить себя и регулярно то ли тормозит, то ли впадает в ступор, то ли не видит очевидных корелляций. Поиск интерфейса полноценного мышления в индивидуальной базе знания - именно то, над чем я сейчас интенсивно работаю. Если Вам интересно, присоединяйтесь! Вся база знаний хранится в виде открытой SQLite базы, к которой легко подключиться из любой среды разработки, из любого языка программирования! Ниже приведена структура основных таблиц системы:
SQL-запросы на создание основных таблиц индивидуальной базы знаний
CREATE TABLE IF NOT EXISTS "author" (
"id" INTEGER NOT NULL UNIQUE,
"name" TEXT NOT NULL UNIQUE,
"notes" TEXT,
"date_time_create" DATETIME,
PRIMARY KEY("id" AUTOINCREMENT)
);
CREATE TABLE IF NOT EXISTS "source" (
"id" INTEGER NOT NULL UNIQUE,
"name" TEXT NOT NULL,
"notes" TEXT,
"source_type_id" INTEGER,
"date_time_create" DATETIME,
"note_show_mode" INTEGER NOT NULL DEFAULT 3,
"status" INTEGER NOT NULL DEFAULT 0,
"cur_stoc_id" INTEGER,
"is_mine" INTEGER NOT NULL DEFAULT 0,
"source_classification_id" integer DEFAULT 0,
"telegram_message_id" TEXT,
"date_time_update" datetime,
PRIMARY KEY("id" AUTOINCREMENT)
);
CREATE TABLE IF NOT EXISTS "note" (
"id" INTEGER NOT NULL UNIQUE,
"subpart_id" INTEGER,
"name" TEXT NOT NULL,
"original_text" TEXT,
"content" TEXT NOT NULL,
"content_plain" TEXT,
"date_time_create" DATETIME,
"fast_text" TEXT,
"note_show_mode" INTEGER NOT NULL DEFAULT 3,
"splitter_param" INTEGER DEFAULT 0,
"h_splitter_param" INTEGER DEFAULT 0,
"date_time_update" DATETIME,
"form_width" integer DEFAULT 840,
"telegram_message_id" TEXT,
"content_html" TEXT,
PRIMARY KEY("id" AUTOINCREMENT)
);
CREATE TABLE IF NOT EXISTS "keyword" (
"id" INTEGER NOT NULL UNIQUE,
"name_main" TEXT NOT NULL,
"date_time_create" DATETIME,
"class_keyword_id" INTEGER NOT NULL DEFAULT 0 CHECK("class_keyword_id" <> "id"),
"order_" integer,
"date_time_update" DATETIME,
"class_name_before" TEXT,
"class_name_after" TEXT,
"name_w_synonym" TEXT,
"note" TEXT,
"all_use_count" integer,
PRIMARY KEY("id" AUTOINCREMENT),
UNIQUE("name_main","class_keyword_id")
);
CREATE TABLE IF NOT EXISTS "keyword_name" (
"id" INTEGER NOT NULL UNIQUE,
"keyword_id" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"order_" INTEGER,
"note" TEXT,
PRIMARY KEY("id" AUTOINCREMENT)
);
CREATE TABLE IF NOT EXISTS "source_author" (
"id" INTEGER NOT NULL UNIQUE,
"source_id" INTEGER NOT NULL,
"author_id" INTEGER NOT NULL,
PRIMARY KEY("id" AUTOINCREMENT),
UNIQUE("source_id","author_id")
);
CREATE TABLE IF NOT EXISTS "note_keyword" (
"id" INTEGER NOT NULL UNIQUE,
"note_id" INTEGER NOT NULL,
"keyword_id" INTEGER NOT NULL CHECK(ifnull("keyword_id", 0) > 0),
"keyword_name_id" integer NOT NULL,
UNIQUE("note_id","keyword_id"),
PRIMARY KEY("id" AUTOINCREMENT)
);
Сейчас у нас тренд на ChatGPT. Но, на мой взгляд, эта технология еще далека от совершенства. А вот если скрестить методы ChatGPT с индивидуальной базой знания, составленной вручную по методу ZettelKasten, то получится вещь! Как утверждает буддизм, мы постоянно говорим сами с собой. Так может быть было бы лучше, чтобы мы говорили с логическо выверенной версией самих себя, которая формируется в индивидуальной базе знания? Именно таким способом Никлас Луман не просто прокрастинировал, а написал более 70 книг и около 500 статей на социологические темы и стал классиком современной социологии.
p.s.: скачать программу для ведения индивидуальной базы знаний можно абсолютно бесплатно вот отсюда: http://www.lumanbox.ru/