写作天下
简介
为作家们创造世界而诞生,执云作笔,诉尽平生意。
集简约UI与人性化AI于一体的码字工具,无论是小说、作文、日记、报告,都能轻松驾驭。
QQ交流群:705849222
特点
已有功能:
自由的目录:自动序号,导入导出
一体化界面:全局自定义主题
输入动画:独创文字平滑输入效果、光标平滑移动
智能引号:增删移动全在一键之间
智能空格:缩进、标点、移动光标融为一体
智能回车:双引号内回车、自动添加句末标点、自动缩进
自动标点:语气词后面自动加上标点符号
同音词覆盖:选词错误,无需删除,直接覆盖旧内容
自动分段:换行/排版时,过长段落自动在合适的地方分段
自动提示:输入时自动提示同义词、相关词、常用句
随机取名:最轻便的取名方式,要啥来啥
一键排版:全自动
未来功能(近期繁忙,无力开发):
大纲列表
章节细纲
名片注释
名字高亮
综合搜索
全书替换
右下角通知卡片
自定义主题
自定义快捷键
小黑屋
云同步
排行榜
拼字房间
章节分享
“求评价”广场
主角背包
快捷键
ctrl + ←/→ 按词语移动
alt + ←/→ 按句子移动
ctrl + alt + ←/→ 按段落移动
ctrl + shift + ↑/↓ 扩大/缩小选择
ctrl/alt + ↑/↓ 屏幕滚动,alt更快
空格键 智能空格
引号键 智能引号
回车键 智能回车
Tab键 句子补全+光标跳过
ctrl+T 一键排版
ctrl+D 句内同音词替换(不必连续)
ctrl+F 章内文字搜索
ctrl+P 全局综合搜索
更多快捷键请等待后期加入(或将支持自定义快捷键)
技术特点
开发环境:C++/Qt5.11.3,Qt Creator
运行环境:Windows、Android
1、标点AI
虽然是简单暴力的枚举(都是自己遇到的),但是有特判了上千种情况,也不容易吧……
若是人物语言描写,还能根据人物的表情动作神态来分析情感程度,进一步提高准确度。
手动判断,故覆盖不全,不过在一定情况下,枚举简单易用,是为上上之选!
还有各种常用按键的自动化操作,极大程度上增加效率!
/**
* 小说语气识别的AI类
*/
class NovelAIBase
{
/*...略了一些方法...*/
void operatorSmartSpace(); // 智能空格:按下空格触发
void operatorSmartEnter(); // 智能回车:按下回车触发
void operatorSmartQuotes(); // 智能引号:按下引号触发
void operatorSmartBackspace(); // 智能删除,按下删除触发
bool operatorAutoPunc(); // 自动标点:语气词自动触发
bool operatorSentFinish(); // 句末标点:增加或转化成结尾
void operatorSmartQuotes2(int left, int right); // 智能引号,仅在选中文本的情况下调用
virtual int getWordCount(QString str); // 字数统计,交给 NovelAI
QString getPunc(QString para/*段落*/, int pos/*光标*/); // ☆核心:取标点(句子)
QString getPunc(/*QString fullText, int pos*/); // 这个是全部文本中的某一部分
QString getPunc(int pos); // 全部文本,特定位置的标点
QString getPunc2(int pos); // 把","改成"。"
QString getPunc2(); // 把","改成"。"
int getDescTone(QString sent); // 句子语气标点,影响语气导向
QString getTalkTone(QString sent, QString sent2, int tone, QString left1, QString left2);
void updateCursorChars(); // 修改光标附近的字符
bool canDeletePairPunc(); // 是否能够删除成对文本(能删就删)
bool isCursorInQuote(QString text, int pos); // 是否在引号里面
virtual void moveCursor(int x) = 0; // 移动光标
virtual void insertText(int pos, QString text) = 0; // 插入文本
virtual void insertText(QString text) = 0; // 插入文本
virtual void deleteText(int start, int end) = 0; // 删除文本
// bool isNextLang(); // 是否是后一句话(双引号前面多的是逗号)
bool isSentPunc(QString str); // 是否为句末标点(不包含引号和特殊字符,不包括逗号)
bool isSentSplitPunc(QString str); // 是否为句子分割标点(包含逗号)
bool isSentSplit(QString str); // 是否为句子分割符(各类标点,包括逗号)
bool isASCIIPunc(QString str); // 是否为英文标点(不包含引号和特殊字符)
bool isBlankChar(QString str); // 是否为空白符
bool isBlankChar2(QString str); // 是否为换行之外的空白符
bool isBlankString(QString str); // 是否为空白字符串
bool isSymPairLeft(QString str); // 是否为对称标点左边的
bool isSymPairRight(QString str); // 是否为对称标点右边的
QString getSymPairLeftByRight(QString str); // 根据右边括号获取左边的括号
QString getSymPairRightByLeft(QString str); // 根据右边括号获取左边的括号
QString getCursorFrontSent(); // 获取当前面的句子
QString getCurrentChar(int x); // 获取当前位置的附近汉字
bool isQuoteColon(QString str); // 汉字后面是否需要加标点
protected:
QString _text, _pre_text; // 文本
int _pos, _pre_pos, _dif; // 光标位置和字数差
QString _left1, _left2, _left3, _right1, _right2; // 光标附近的文本
bool isInQuotes; // 是否在引号里面(用来判断是否为语言或者描述)
private:
QString _shuo_blacklists, _dao_whitelists, _wen_blacklists; // “说”黑名单、“道”白名单、“问”黑名单
QString _symbol_pair_lefts, _symbol_pair_rights; // 成对符号左边/右边
QString _quote_no_colon, _quantifiers; // 引号前面没有冒号
QString _sent_puncs, _sent_split, _sent_split_puncs, _blank_chars; // 句末标点、句子分割符、空白符
QString _auto_punc_whitelists; // 自动标点白名单
};
子类为NovelAI,再下一个子类 NovelEditor 使用到了多继承,同时继承 QTextEdit 与 NovelAI。
其实设计得不是很好,AI 类需要大量调用 Edit 的方法,于是加了许多虚函数,将三个类紧紧耦合到一起(不过关系不大,本来就是怕一个文件代码太长而分开的)。
2、仿IDE思路
当今所谓的写作软件,单论写作方面,其实和一个记事本没多少差别,无非多了目录与自动缩进,以及简单的自动保存等。其一些亮点,比如强制写作的小黑屋、多人竞争的在线拼字,都无法从“码字”本身来帮助作者达到更高效的办公,这些附加的功能甚至会让作者分心,无法好好静心创作。
写作天下定位一款单纯的“文学创作”编辑器,主打从“写作”本身提高效率,弱化与文字无关的功能。或许后期将会添加小黑屋、拼字等,但绝不会将这些作为主要功能。
备注:近期繁忙,无力开发,所以只是“思路”!
一些功能:
增强的编辑功能
自动提示
面向对象写作:名片系统
文字高亮(名片高亮)
高度个性化设置(功能细节、快捷键、主题等)
追求自动的编辑器
一句话:能自动的,绝不手动!
为了实现更加人性化的功能,程序中使用了大量的算法。
比如括号匹配功能,输入左括号时自动添加右括号、删除键删除成对的符号,这里使用数据结构中的栈,判断光标前后各自左右括号的数量,入栈出栈,来判断是否需要添加/删除,而不仅仅依靠光标前一个字符。
还有光标移动、在标点前面换行、在语言描写内回车自动插入前后引号、修改引号前面的逗号与冒号等,一个按键,多种功能。
轻松提示
经过多种方案的性能测试,选取了速度最快、运行最稳定的方式。
以下为输入文字后自动提示算法:
/**
* 某个句子的某个位置处进行搜索
* @param sent 欲搜索的完整句子(短句,不包含标点)
* @param cursor 光标在句子中的相对位置
* @return 是否找到
*/
bool Lexicons::surroundSearch(QString sent, int cursor)
{
int len = sent.length();
search_result.clear();
match_sentence = false;
bool find = false;
matched_key = "";
int start_pos = 0;
QString l1 = "", l2 = "", l4 = "";
if (cursor >= 1)
l1 = sent.mid(cursor-1, 1);
if (cursor >= 2)
l2 = sent.mid(cursor-2, 2);
if (cursor >= 4)
l4 = sent.mid(cursor-4, 4);
if (random_inited)
{
// 随机种类列表
if (((matched_key = l2) == "随机")
|| ((matched_key = l2) == "取名")
|| ((matched_key = l4) == "随机取名"))
if (searchRandom("随机取名"))
{
matched_case = COMPLETER_CONTENT_RAND_LIST;
return true;
}
// 姓氏
if (surname_inited && (((matched_key = l1) == "姓")
|| ((matched_key = l2) == "姓氏")))
if (searchRandom("姓氏"))
{
matched_case = COMPLETER_CONTENT_SURN;
return true;
}
// 人名
if (name_inited && (((matched_key = l2) == "人名")
|| ((matched_key = l2) == "名字")))
if (searchRandom("人名"))
{
matched_case = COMPLETER_CONTENT_NAME;
return true;
}
// 随机列表
for (QString s : random_sort_list)
{
if (cursor >= s.length() && sent.mid(cursor-s.length(), s.length()) == s && isFileExist(lexicon_dir + "random/" + s + ".txt"))
{
matched_key = s;
matched_case = COMPLETER_CONTENT_RAND;
searchRandom(s);
return true;
}
}
}
// 搜索4个字
if (!find)
{
start_pos = 0; // 开始搜索的位置
if (start_pos < cursor-4) start_pos = cursor-4;
for (int i = start_pos; i <= len-4 && i < cursor; i++)
{
if (search(sent.mid(i, 4), true))
{
find = true;
matched_key = sent.mid(i, 4);
break;
}
}
}
// 搜索三个字
if (!find)
{
start_pos = 0;
if (start_pos < cursor-3) start_pos = cursor-3;
for (int i = start_pos; i <= len-3 && i < cursor; i++)
if (search(sent.mid(i, 3), true))
{
find = true;
matched_key = sent.mid(i, 3);
break;
}
}
// 搜索两个字
if (!find)
{
start_pos = 0;
if (start_pos < cursor-2) start_pos = cursor-2;
for (int i = start_pos; i <= len-2 && i < cursor; i++)
if (search(sent.mid(i, 2), true))
{
find = true;
matched_key = sent.mid(i, 2);
break;
}
}
//qDebug() << "surround search:" << matched_key << " result:" << search_result;
search_last = sent;
matched_case = COMPLETER_CONTENT_WORD;
return find;
}
/**
* 在词库中搜索某一个词语
* @param key 欲搜索的词语
* @param add 是否为添加模式。如果不是,则先清空已经找到的列表
* @return 是否有搜索结果
*/
bool Lexicons::search(QString key, bool add)
{
if (key.isEmpty()) return false;
/* // 上次搜索的缓冲区,但是没必要了,因为每次surroundSearch的时候
* // 都把上次的搜索结果清空了,key还在但是result没了
if (key == search_last)
{
return true;
}*/
if (!add) search_result.clear();
bool find = false;
int key_len = key.length();
if (synonym_inited)
{
QStringList synonym_list;
int pos = 0;
while (1)
{
//pos = synonym_text.indexOf(key, pos);
pos = synonym_text.indexOf(QRegExp("\\b"+key+"\\b"), pos);
if (pos == -1) break;
int left = synonym_text.lastIndexOf("\n", pos)+1;
int right = synonym_text.indexOf("\n", pos);
if (right == -1) right = synonym_text.length();
QString para = synonym_text.mid(left, right-left);
QStringList list = para.split(" ", QString::SkipEmptyParts);
// TODO 后期将改成 QList
synonym_list.append(list);
pos += key_len;
find = true;
}
if (synonym_list.size() > 0 && shouldRandom())
{
if (shouldRandom())
{
std::random_shuffle(synonym_list.begin(), synonym_list.end());
}
search_result.append(synonym_list);
}
}
if (related_inited)
{
QStringList related_list;
int pos = 0;
while (1)
{
pos = related_text.indexOf(QRegExp("\\b"+key+"\\b"), pos);
if (pos == -1) break; // 找不到了
if (pos > 0 && related_text.mid(pos-1, 1) == "{") // 是标题
{
int left = related_text.indexOf("[", pos)+1;
int right = related_text.indexOf("]", pos);
if (right < left-1) // 出现了错误
{
pos = left+1;
continue;
}
if (right == -1) right = related_text.length();
QString para = related_text.mid(left, right-left);
QStringList list = para.split(" ", QString::SkipEmptyParts);
// TODO 后期将改成 QList
related_list.append(list);
pos = right;
}
else // 是内容
{
// 如果是本程序标准格式
int left = related_text.lastIndexOf("[", pos)+1;
int right = related_text.indexOf("]", pos);
// 如果只是一段一段分开的
int left_n = related_text.lastIndexOf("\n", pos)+1;
if (left_n >= left)
{
left = left_n;
right = related_text.indexOf("\n", pos);
}
if (right == -1) right = related_text.length();
QString para = related_text.mid(left, right-left);
QStringList list = para.split(" ", QString::SkipEmptyParts);
// TODO 后期将改成 QList
related_list.append(list);
pos = right;
}
find = true;
}
if (related_list.size() > 0)
{
if (shouldRandom())
{
std::random_shuffle(related_list.begin(), related_list.end());
}
search_result.append(related_list);
}
}
if (sentence_inited && find)
{
if (sentence_text.indexOf(key) > -1)
{
match_sentence = true;
search_result.append("-->");
}
}
//qDebug() << "search:" << key << " result:" << search_result;
return find;
}
/**
* 通过一个词语,来获取应该显示的随机取名提示列表
* @param key 欲搜索的词语
* @return 是否找到结果
*/
bool Lexicons::searchRandom(QString key)
{
// ==== 随机取名列表 ====
if (key == "随机取名" || key == "随机" || key == "取名")
{
for (QString s : random_sort_list)
search_result.append(s);
std::random_shuffle(search_result.begin(), search_result.end());
return true;
}
// ==== 随机取名具体 ====
for (int i = 0; i < random_sort_list.size(); i++)
if (random_sort_list.at(i) == key)
{
QStringList list = random_text_list.at(i).split(" ", QString::SkipEmptyParts);
search_result = list;
std::random_shuffle(search_result.begin(), search_result.end());
search_result = search_result.mid(0, 100);
return true;
}
return false;
}
/**
* 搜索后返回结果
* @return 搜索结果
*/
QStringList Lexicons::getResult()
{
return search_result;
}
3、平滑输入
创作,或是为了宁静的内心,或是为了美好的生活,终是需要解放内心的压抑。
而写作天下的输入动画与平滑光标,为了打造愉悦的创作环境,可谓是呕心沥血。
动画算法
输入动画
输入动画中每一个字符都是一个Label对象,设置文字颜色为对应字符,然后使用一个动画管理器进行统一管理,开放管理器的API给编辑器,编辑文字的同时通过管理器调整所有文字的动画,便不会出现冲突的情况。
难点在要将每个Label和对应文字连在一起,尤其是支持实时修改。看了其他相同功能的开源项目,大多只支持英文,并且是监听到文字输入后才开始动画,等动画结束再上屏,无法在输入后立即删除或者修改,非常影响效率。
/**
* 输入动画管理器类
*/
class EditorInputManager : public QObject
{
Q_OBJECT
public:
EditorInputManager();
void setEditor(QTextEdit* edit); // 设置开启动画对应的编辑器
void setColor(QColor color); // 设置全文颜色
void textChanged(int old_position, int diff); // 文字改变时调整动画控件
void updateRect(int range_start, int rande_end); // 更新动画控件的位置
void addInputAnimation(QPoint point, QString str, int position, int delay, int duration); // 添加一个文字动画
void addInputAnimation(QPoint point, QString str, int position, int delay, int duration, QColor color); // 添加一个带有不同颜色的文字动画
void updateTextColor(int current_position); // 修改全文颜色,同时修改正在动画的颜色
public slots:
void aniFinished(int position, EditorInputCharactor *); // 动画结束,传参文字位置
private:
QTextEdit* _edit; // 编辑器
QColor font_color; // 全文颜色
QList ani_list; // 动画控件列表
};
/**
* 输入动画的某一个字符实体,存储动画信息、光标位置、光标坐标、光标文字等
*/
class EditorInputCharactor : public QLabel
{
Q_OBJECT
Q_PROPERTY(int fontsize READ getFontSize WRITE setFontSize RESET resetFontSize)
public:
EditorInputCharactor(QWidget* parent, QPoint point, QString str, int position, QFont font, int delay = 0, int duration = 200);
int getPosition(); // 获取动画的文字位置,更新位置、动画结束文字变回原色时要用到
void changePosition(int x); // 修改动画的位置
void updateRect(QPoint point); // 更新控件坐标
// 字体动画接口
void setFontSize(int x);
int getFontSize();
void resetFontSize();
signals:
void aniFinished(int position, EditorInputCharactor* charactor); // 动画结束
private:
QPoint point; // 坐标
QString str;
int position; // 文字位置
int duration; // 动画时长,同时输入的不同位置的文字时长不一样
QPropertyAnimation *ani; // 动画对象
int origin_position; // 一开始的位置
int font_size; // 文字大小动画属性
int font_size_l;
};
平滑光标
平滑光标添加一个矩形控件为光标样式,每次改变时存储其坐标,然后借助动画移动控件。
其实还简单,但难点在要好好控制什么时候才开始动画,不然光标会乱飘或者闪动,看起来效果很差。
最大的难点在于,与功能“光标行固定”以及“底部光标固定”有很大的冲突,还有每次修改文字时(这是自定义事件经常使用QTextEdit.setText()方法才会出现的问题)需要大大约束光标动画的时机。
4、MVD目录结构
Qt标准 QListView 的MVD模型,一开始以为很难,刚走来发现其实就是重写几个方法。
这里使用的 MVD似乎与标准的不太一样,看下去就知道了。
Model
由于写作天下的目录比较复杂,所以专门写了一个类NovelDirMData来存放多个NovelDirItem的信息。
此处 Model 用到了多继承,以及友元。(其实这个模型已经变了味了)
class NovelDirModel : public QAbstractListModel, public NovelDirMData
{
/*...其余代码略...*/
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const
{
// 不需要 index 的 data
if (role == Qt::UserRole+DRole_ROLL_COUNT)
{
return roll_subs.size();
}
else if (role == Qt::UserRole+DRole_CR_COUNT)
{
return cr_list.size();
}
else if (role == Qt::UserRole+DRole_ROLL_NAMES)
{
return roll_names;
}
// 判断 index
if ((!index.isValid()) || (index.row() >= cr_list.size()))
return QVariant();
NovelDirItem item = cr_list.at(index.row());
if (role == Qt::UserRole+DRole_CHP_NAME)
{
return item.getName(); // 章节名
}
else if (role == Qt::UserRole+DRole_CHP_NUM)
{
return item.getNumber(); // 序号
}
else if (role == Qt::UserRole+DRole_CHP_NUM_CN)
{
return item.getNumber_cn(); // 序号
}
else if (role == Qt::UserRole+DRole_CHP_ROLLINDEX)
{
return item.getRollIndex(); // 序号
}
else if (role == Qt::UserRole+DRole_CHP_CHPTINDEX)
{
return item.getChptIndex(); // 序号
}
else if (role == Qt::UserRole+DRole_CHP_DETAIL)
{
return item.getDetail(); // 细纲
}
else if (role == Qt::UserRole+DRole_CHP_OPENING)
{
return item.isOpening(); // 是否编辑中
}
else if (role == Qt::UserRole+DRole_CHP_ISROLL)
{
return item.isRoll(); // 是否为分卷
}
else if (role == Qt::UserRole+DRole_CHP_ISHIDE)
{
return item.isHide(); // 是否隐藏
}
else if (role == Qt::UserRole+DRole_CHP_STEMP)
{
return item.getS_temp(); // 双击编辑,出错后恢复原来的文本
}
else if (role == Qt::UserRole+DRole_ROLL_SUBS)
{
int rIndex = item.getRollIndex();
return roll_subs[rIndex]; // 分卷章数量
}
else if (role == Qt::UserRole+DRole_ROLL_COUNT)
{
return roll_subs.size();
}
else if (role == Qt::UserRole+DRole_CR_COUNT)
{
return cr_list.size();
}
else if (role == Qt::UserRole+DRole_CHP_FULLNAME)
{
return item.getFullChapterName();
}
else if (role == Qt::UserRole+DRole_RC_ANIMATING)
{
return item.isAnimating();
}
else if (role == Qt::UserRole+DRole_ROLL_NAMES)
{
return roll_names;
}
else if (role == Qt::UserRole+DRole_RC_SELECTING)
{
return item.isSelecting();
}
return QVariant();
}
// 重写 flags 和 setData 使 Model 可双击编辑
Qt::ItemFlags flags(const QModelIndex &index) const
{
Qt::ItemFlags flags = QAbstractItemModel::flags(index);
//if (index.row() > 0 && us->one_click) // 作品相关卷名不允许更改
//flags |= Qt::ItemIsEditable;
if (index.row() > 0)
flags |= Qt::ItemIsEditable;
return flags;
}
bool setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!index.isValid()) return false;
NovelDirItem item = cr_list.at(index.row());
if (role == Qt::EditRole) // 修改章节名
{
QString old_name = item.getName();
bool isNew = false;
if (canRegExp(old_name, "新[章卷]\\d+")) // 分卷是聚集焦点用
isNew = true;
bool rst = tryChangeName(index.row(), value.toString());
if (/*isNew && */rst) // 重命名成功
{
if (isNew) // 如果是新建的
emit signalOpenChapter(index.row());
else // 否则就是重命名,修改已经打开的旧名字标签页
emit signalChangeFullName(novel_name, value.toString());
}
return rst;
}
else if (role == Qt::UserRole+DRole_CHP_STEMP)
{
cr_list[index.row()].setS_temp(value.toString());
}
else if (role == Qt::UserRole+DRole_CHP_FULLNAME)
{
cr_list[index.row()].setFullChapterName(value.toString());
deb(value.toString(), "model.setFullChapterName");
}
return true;
}
}
View
这里重写的方法有点多……就放一个设置Model和Delegate的。
不是很标准,因为为了方便,把应该解耦的Model和Delegate都声明为成员变量了。
// 设置 delegate
novel_dir_delegate = new NovelDirDelegate(this);
setItemDelegate(novel_dir_delegate);
// 设置 model
novel_dir_model = new NovelDirModel(this);
setModel(novel_dir_model);
Delegate
因为用到了作品设置,有用户自定义的完全不同的目录结构,所以和Model一样使用了多继承。
有三大特色:
用户自定义文字显示:例如,阿拉伯数字自动排序、不显示分卷等
圆形显示效果:不知道该怎么描述……至少要比默认的蓝色选中效果好看吧?
支持全局主题色/点缀色,实时修改
/**
* 目录列表代理类
*/
class NovelDirDelegate : public QItemDelegate, public NovelDirSettings
{
/* ... 略了一些 ... */
QString getItemText(const QModelIndex &index) const
{ /*...获取章节序号,略...*/}
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
QStyleOptionViewItem op(option);
op.palette.setColor(QPalette::Highlight, us->getOpacityColor(us->accent_color, 128));
painter->save();
// 获取文字
QString text= getItemText(index);
int deviate = DIR_DEVIATE; // 初始偏移位置
int subs = -1; // 分卷数量,章节为-1
if (index.data(Qt::UserRole+DRole_CHP_ISROLL).toBool()) // 是分卷,画数字
{
subs = index.data(Qt::UserRole+DRole_ROLL_SUBS).toInt();
}
else // 是章节
{
if (isNoRoll() && index.data(Qt::UserRole+DRole_CHP_NUM).toInt() > 0) // 不显示分卷 且 是正文,则取消缩进
;
else
deviate += DIR_DEVIATE_ADD; // 增加章节的缩进
}
// 获取文字区域并绘制
QFontMetrics fm(painter->font());
QRect text_rect = QRect(op.rect.topLeft()+QPoint(deviate,(op.decorationSize.height()+us->mainwin_sidebar_spacing*2-fm.height())/2),QSize(fm.width(text),fm.height()));
if (!us->round_view)
drawBackground(painter, op, index); // 画选中的颜色的(不加这行的话就是透明选中)
// 先绘制背景
if (!index.data(Qt::UserRole+DRole_RC_ANIMATING).toBool()) // 不是在动画中(动画时不显示文字,即背景透明)
{
// 绘制选中状态的纸片形状背景
if (us->round_view && (option.state & QStyle::State_Selected))
{
int text_padding = text_rect.height()/2-1;
QColor bg_color = us->getOpacityColor(us->accent_color, 128);
QRect round_rect(text_rect.left()-text_padding, text_rect.top()-3, text_rect.width()+text_padding*2, text_rect.height()+6);
painter->setRenderHint(QPainter::Antialiasing);
QPen pen(bg_color, Qt::NoPen);
painter->setPen(pen);
QPainterPath path;
int radius = round_rect.height()/2-1;
path.addRoundedRect(round_rect, radius, radius);
painter->fillPath(path, bg_color);
painter->drawPath(path);
painter->setRenderHint(QPainter::Antialiasing, false);
}
// 绘制文字
if (isNoRoll() && subs >= 0) // 不使用分卷,并且刚好是分卷,则使用灰色,弱化分卷存在感
{
painter->setPen(QColor(128, 128, 128));
}
else
painter->setPen(QColor(0, 0, 0));
painter->drawText(text_rect, text);
// 画左边的小标记
if (subs >= 0) // 是分卷(章节默认 -1)
{
}
else // 是章节,绘制打开状态
{
if (index.data(Qt::UserRole+DRole_CHP_OPENING).toBool()) // 打开状态中,画右边的小点
{
QRect state_rect = QRect(op.rect.topLeft()+QPoint(0,1),QSize(1,op.rect.height()-2));
painter->setPen(QPen(QColor(us->accent_color), Qt::SolidLine));
painter->setBrush(QBrush(QColor(us->accent_color), Qt::SolidPattern));
painter->drawRect(state_rect);
}
}
// 画右边的小标记,分卷数量
if (subs >= 0) // 是分卷(章节默认 -1)
{
if (subs == 0) // 没有章节,灰色的数字
{
painter->setPen(QColor(128, 128, 128));
}
else // 章节,黑偏灰色的
{
painter->setPen(QColor(64, 64, 64));
}
QString subs_text = QString("%1").arg(subs);
QRect display_rect2 = QRect(op.rect.topRight()-QPoint(fm.width(subs_text)+10,-3),QSize(fm.width(subs_text),fm.height()));
painter->drawText(display_rect2, subs_text);
}
}
// drawFocus(painter, option, displayRect); // 画选区虚线
drawFocus(painter, op, QRect(0, 0, 0, 0));
painter->restore();
}
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
Q_UNUSED(index);
//QSize size = option.rect.size(); // 这个是整个QListView的矩形……
QSize size = option.decorationSize;
size.setHeight(size.height()+us->mainwin_sidebar_spacing*2);
return size;//QItemDelegate::sizeHint(o2, index);
}
/* 编辑框修改名字 */
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex&index) const
{
Q_UNUSED(option);
Q_UNUSED(index);
QLineEdit* edit = new QLineEdit(parent);
edit->setAcceptDrops(false);
QString style = "";//"background-color:"+us->getColorString(us->accent_color)+";";
if (us->round_view)
{
QString text= getItemText(index);
QFontMetrics fm(edit->font());
int r = fm.height()/2;
QString rs = QString("%1").arg(r);
style += "border:1px;border-radius:"+rs+"px; padding-left: "+rs+"px;";
}
else
style += "border:none;";
edit->setStyleSheet(style);
QPalette palette = edit->palette();
if (us->editor_font_color.alpha() > 0)
palette.setColor(QPalette::Text, us->editor_font_color);
palette.setColor(QPalette::Base, us->accent_color/*editor_bg_color*/);
if (us->editor_font_selection.alpha() > 0)
palette.setColor(QPalette::HighlightedText, us->editor_font_selection);
palette.setColor(QPalette::Highlight, us->editor_bg_selection);
edit->setPalette(palette);
return edit;
}
void setEditorData(QWidget *editor, const QModelIndex &index) const
{
QString name = index.data(Qt::UserRole+DRole_CHP_NAME).toString();
if (index.data(Qt::UserRole+DRole_CHP_STEMP).toString() != "")
name = index.data(Qt::UserRole+DRole_CHP_STEMP).toString();
QLineEdit* edit = static_cast(editor);
// renameEdit = edit;
edit->setText(name);
}
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
QLineEdit* edit = static_cast(editor);
QString str = edit->text();
//closeEditor(edit, QAbstractItemDelegate::NoHint);
if (model->setData(index, str))
model->setData(index, str, Qt::UserRole+DRole_CHP_STEMP);
else
model->setData(index, str, Qt::UserRole+DRole_CHP_STEMP);
}
void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
Q_UNUSED(index);
int deviate = DIR_DEVIATE-2;
if (!(index.data(Qt::UserRole+DRole_CHP_ISROLL).toBool()))
deviate += DIR_DEVIATE_ADD;
if (us->round_view)
{
QString text= getItemText(index);
QFontMetrics fm(editor->font());
int delta = fm.height()/2;
deviate -= delta;
}
QRect rect = option.rect;
rect.setLeft(rect.left()+deviate);
editor->setGeometry(rect);
}
QLineEdit* getEditor()
{
return rename_edit;
}
private :
QLineEdit* rename_edit;
};
使用这个代理后,发现效果还是挺好看的,比原先不知道好了多少。