摸鱼用txt阅读器(JavaFX版)

Posted on 2022-05-01 15:32  Capterlliar  阅读(707)  评论(0编辑  收藏  举报

想在电脑上看小说。至于为什么用JavaFX那当然是因为我不会Qt啦。

Github链接


 upd 2023.11.12

今天突然发现打不开了,会报错:Cannot invoke "com.sun.javafx.font.directwrite.IWICImagingFactory.CreateBitmap(int, int, int, int)" because "<local3>" is null

查了一下可能是Windows10 22H2 19045.3570和JavaFX 12哪里不兼容,更换JavaFX版本可解决,目前17.0.1可用。Java版本:Java16

相关报错讨论:0.3.2 running on Windows10 (#5) · 议题 · Elad / SeratoCrateExporter · GitLab


upd 2022.10.1

前排预警:这份代码写得很拉,唯一的优点是能跑。

最近阅读量暴涨100。很恐慌。


 

0.前言

本来想一边做一边写的,但开工的时候没个规划,写得乱七八糟,一会儿改一遍容器,最后直接合并了两个类。写完了才想起maven框,又调半天。这个故事告诉我们写代码前先画个图理理逻辑是很重要的。

不过从头自己设计一个小软件还是很开心的啦qwq

1.功能

  • 打开本地小说,并解析出目录;
  • 无边框,可调节背景透明度(便于隐藏摸鱼;
  • 支持键盘翻页(假装钻研代码

2.运行流程

  1. 打开上次缓存记录,若没有,则提醒没有打开的书;
  2. 加载小说,如果没有看过,解析目录,按章节始末位置存储,便于下次打开;如果没有分章或者没解析出来,则将内容分段存储。如果看过,直接打开上次的缓存文件;
  3. 加载UI界面;
  4. 每次加载一章,显示当前内容;换章时加载下一章内容;
  5. 关闭时存储当前进度

3.类的说明

  • Chapter    记录章节在txt中的起始位置、结束位置和章节名。
  • Book    包含文件名、路径、章节、解析章节是否成功。方法包括解析章节并缓存、读入指定章节等。
  • BookTemp    用于缓存上次阅读信息。
  • GUI    主类。构建场景,添加节点。
  • Chapter_GUI    获取当前章节内容并显示到界面中。这名起的不好。
  • Apprun    用于启动程序,避免很多加载问题。

        工具类:

  • DragUtil    用于在Stage取消装饰后添加拖动功能。
  • DrawUtil    用于添加拖拽功能。
  • 以上两个工具类作者为Light,原博链接

4.部分问题解决方法

按写代码的顺序来。

1.Book类

啊这个类。一开始真的只有Book的模型,处理文本是另一个叫DealText的类(纪念一下它),后来传来传去的重复很多,索性和在一起了。

先说文件读写。考虑到要便于从中抽取章节,用了RandomAccessFile类。

使用方法:

RandomAccessFile in=new RandomAccessFile(filepath,"r");
long start=0;
in.seek(start);//指 针 跳 动
t=in.readLine()
now_pos=in.getFilePointer();//获取当前位置,为long型
in.length()//全部文本长度
in.close();

判断章节的研究了很久小说的正则表达式:

String r="\\s*(第[0-9|零一二三四五六七八九十壹贰叁肆伍陆柒捌玖拾百千万]+[章卷回话]" +
                "|[c|C]hapter.*[0-9]|☆、.*|[上中下终]卷" +
                "|卷[0-9|零一二三四五六七八九十壹贰叁肆伍陆柒捌玖拾]+" +
                "|[引子|楔子|序][\n\\s]|[Ll][Vv].[0-9]+|-Quiz [0-9]+).*";

正则表达式,小括号表示匹配几项,中括号表示从里面选择。

正则表达式使用:

Pattern p=Pattern.compile(r);
Matcher m=p.matcher(now_text);
if(m.matches())

考虑到每读完一行指针停留在末尾,也就是正则表达式判完了的位置实际上是标题结束的位置,再记录一下上一行末尾的pos,匹配上用这个。

考虑到每行都匹配一遍很慢,经过我仔细观察章节名是不应当有句号的。但很多人物对话结尾都用,所以再筛一波:“

有时候明明有第x章,但匹配失败,很可能是中英文空格不同的问题。因此对于要匹配的文字先把中文空格换成英文空格,最后用trim()去掉首尾空格。

now_text=now_text.replaceAll(" "," ").trim();

读取特定章节也差不多,就不写了。BookTemp没几行也没啥好说的。

2.Chapter_GUI类

写这个类的时候有俩需求,一是方便大小调整,二是能区分标题和正文的字体大小。因此在传参的时候加入了容器宽高,字号大小,以及有没有标题。说实话规定宽高初始化的时候挺不方便的,别人绑定一下就好了,这还得手动乘比例。但JavaFX一开始运行它没有宽高,得自己设置,或者是有但我没找到。如果有人看且看的人知道答案记得踢踢我(

接下来找了半天也没找到合适的显示文字的容器。先按宽度把原文分行,放到队列里,然后以VBox为底,在上面添加Text。注意每放一句话就要开一个新的Text,不然VBox会以为你把同一个添加了好几遍,然后报错。计算当地Text高度可以用这个函数:

text.getBoundsInLocal().getHeight();

算宽度的时候不知道这玩意,就用的对照表里的宽度,所以最后做出来右边有空当,但问题不大。

每次换页的时候要先把之前的内容clear()掉。

3.GUI类

啊这个类太恐怖了。

目标:

    • 能拖动、改变大小;
    • 实现翻页功能;
    • 能点击目录转到对应章节;
    • 能从本地选择小说
    • 关闭及最小化
    • 调节透明度

如果呢把拖动和拖拽绑在一个容器上,那么拖拽功能是会被覆盖的;因此选择在background上再加一个容器,放目录栏和正文。

考虑background的选择。CenterPane看起来不错,但其子节点大小过于玄学;想要有并列的两个box,可以考虑HBox,但HBox不能确定子节点位置,因此最后用了朴实无华的Pane(。

设置宽高:

background.setPrefHeight(prefsize);
background.setPrefWidth(prefsize);

  以前不理解能写一个数的为什么要特地给它一个变量,现在我懂了,这个数还可能会出现n次,改起来那是真的死亡。

Tips:

idea中可以Ctrl+R搜索和修改变量名。如果只是找哪里出现点一下就好了。

绑定拖拽事件:

DrawUtil.addDrawFunc(stage,background);

background上开一个HBox放目录和正文,留个边给拖拽:

hBox.layoutXProperty().bind(background.widthProperty().multiply(5).divide(100));
hBox.layoutYProperty().bind(background.heightProperty().multiply(5).divide(100));
hBox.prefHeightProperty().bind(background.heightProperty().multiply(0.9));
hBox.prefWidthProperty().bind(background.widthProperty().multiply(0.9));

绑定后mutiply一下,就是占比多少的意思。

然后一样的开一个Pane放页面的vBox,和另一个Pane用来点击出现目录。由于点击目录后目录会占一定比例,而我不希望目录直接覆盖部分页面,这样谁知道点到哪了。所以页面的Pane会有两个比例。初始化先绑定pane_width1,起个全局变量,方便之后调整。

最后加一个listview来展示目录。

先把章节名都放到FX自带的Collection里,然后统一加进去。调整一下格子大小。

ObservableList<Label> list = FXCollections.observableArrayList();
for (int i = 0; i < book.getChapter_num(); i++) {
     list.add(new Label(book.getChapterName(i)));
}
listView = new ListView<>();
listView.setItems(list);

listView.setFixedCellSize(20);

listview的css麻烦一点,要分背景、奇数格子、偶数格子

.list-cell:even{
    -fx-background-color:rgba(123,186,238,0.1);
    -fx-text-fill: black;
}
.list-cell:odd{
    -fx-background-color:rgba(255,255,255,0);
    -fx-text-fill: black;
}
.list-view {
    -fx-background-color: transparent;
    -fx-text-fill: black;
}
.list-cell:selected {
    -fx-background-color:rgba(124,124,124,0.6);
    -fx-text-fill: black;
}
.list-cell:hover{
    -fx-background-color:rgba(124,124,124,0.5);
    -fx-text-fill: black;
}

介于最后要调节透明度,很多透明度为0.1的叠在一起就不透明了,所以除了background都设置rgba(x,x,x,0)。不能用设置opacity替代,不然文字内容就没了。

关于为什么不统一绑定css,因为用Application那个方法slide会显示不出来。

用slide调节透明度。每当你想不到用什么元件可以打开Scene Builder,里面啥都有。

设成横的:

slider.setOrientation(Orientation.HORIZONTAL);

提醒没有前/后一页了:用一个TimeLine,设定一帧一帧的动画,达到自己出现自己消失的目的:

Timeline t = new Timeline(
         new KeyFrame(Duration.seconds(0), new KeyValue(label.opacityProperty(), 1)),
         new KeyFrame(Duration.seconds(4), new KeyValue(label.opacityProperty(), 0))
        );
t.setAutoReverse(true);
t.setCycleCount(Timeline.INDEFINITE);
t.play();

TimeLine文档

好了基本内容有了,接下来绑定事件。

关于JavaFX事件相应的逻辑:点这里

一些可以用的事件:点这里

其实官方文档还是最方便的。但有中文谁不想看中文呢(

首先是翻页。我希望鼠标点击Pane的左右两侧和用键盘上下左右都能翻页,因此设两个事件。由于Slider也是自带键盘事件的,所以要提前把这个事件消耗掉,键盘事件设在scene那里。

    scene.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
            if (e.getCode() == KeyCode.RIGHT || e.getCode() == KeyCode.DOWN)
                page_num++;
            if (e.getCode() == KeyCode.LEFT || e.getCode() == KeyCode.UP)
                page_num--;
            turnPage();
            e.consume();
        });

点击事件就在pane里。

        pane.setOnMouseClicked(e->{
            if(stage_X==stage.getX()&&stage_Y==stage.getY()) {
                double x = e.getSceneX();
                if (x < pane.getWidth() / 2)
                    page_num--;
                if (x >= pane.getWidth() / 2)
                    page_num++;
                turnPage();
            }
            else {
                stage_X = stage.getX();
                stage_Y = stage.getY();
            }
            e.consume();
        });    

由于pane上还有一个拖动事件,所以判断一下舞台位置有没有移动,没有的话判断为翻页。因此还要记录舞台位置,加俩变量维护一下。

翻页的方法注意前后章节衔接。往前一章在最后一页,往后一章在第一页。

关于拖拽事件,大小变了文字排版也得变啊,每次重新加载一下章节还是可以接受的,但是每次变化途中都要加载是不能接受的。所以不用ChangeListener,当鼠标放下再重新加载页面,注意页面大小调整后当前页码是否仍然存在:

    background.setOnMouseReleased(mouseEvent -> {
            //根据窗口大小判断是否改变文字还是翻页
            if(back_width!=background.getWidth()||back_height!=background.getHeight()) {
                back_width=background.getWidth();
                back_height=background.getHeight();
                chapter.setSize(pane.getWidth(), pane.getHeight());
                page_num=Math.min(page_num,chapter.getPage_num()-1);
                pane.getChildren().clear();
                pane.getChildren().add(chapter.getPage(page_num));
                mouseEvent.consume();
            }
        });

还是要特判一下翻页,毕竟鼠标事件对于程序来说没有区别,它也不知道你点在哪。

catalog那里每次都加载一遍目录没必要,所以第一次加载后就存在那,后面直接调用即可。不用设为null。绑定的事件没啥。

Slider要加个listener:

slider.valueProperty().addListener((ov,oldval,newval)->{
            background.setStyle("-fx-background-color:rgba("+background_color+newval+")");
        });

文件处理和缓存是点细节,懒得写了。

4.关于jar包和调试时文件路径不同的问题

打包又查了半天,最后加了marven一键package,舒服了。

marven项目指定路径

然后用exe4j装成exe,打不开,找不到文件。

 研究了一下java的文件默认目录。

File那一堆默认是项目根目录

getClass.getResource和getResourceAsStream不加/是当前文件目录,加了是源根目录。

但是getResource到了jar包里就啥都检测不到啦!(upd补:也许找得到,可能是我放的地方有问题。

因此图片改成:

stage.getIcons().add(new Image(getClass().getResourceAsStream("/cat.png")));

  文件改成:

String tempfile=System.getProperty("user.dir")+test+"/resources"+"/lastview";
tempfile=tempfile.replaceAll("\\\\","/");

  就css事最多,要url,之前一直用toExternalForm,现在手写url,

String cssfile="file:/"+System.getProperty("user.dir")+test+"/resources"+"/GUI.css";
cssfile=cssfile.replaceAll("\\\\","/");

  本地跑了,但到有jre的朋友电脑上又不跑了,难绷。等完修修。

  参考资料:

  读取jar包内外文件

  getResource return null

  java如何读取jar包中的资源和路径

5.Profiler的使用

听说javafx写出来的应用比较占运行内存,于是找了个内存检测软件。

JProfiler12的下载

然后在IDEA里装个同名插件

   是这样的。

先点左边的按钮,它加载完会告诉你等待连接,然后我等了半天发现并没有连上,实际应该再点右边的图标手动连接。打扰了。

出现界面后直接点attach,找到正在运行的项目,然后一路确定。

一个教程

upd补:java有垃圾自动回收机制,写点gc的意义可能在于,看到它马上就回收,心理安慰。

inf. 后续以及遗留问题

  • 写完想打包成exe发给朋友一起摸鱼的,但让人家下jre也不方便,想打包一起打进去。试了jpackage,不知道运行到哪里去了;javafx似乎在marven上有个打包带jre的插件,但跑不起来,可能是版本问题,国内资料都有点过时,搜javafx还得上google;最后放弃治疗直接一个jre提出来放exe4j里,发出去还是找不到类。累了。先自己凑合用用吧。
  • 任务管理器看内存高达117MB,但用JProfiler检测只有14MB,很困惑啊。
    •   ↑upd:运行内存核提交内存和任务管理器内存是三个东西
  • 老忘空指针处理,这个不好,随时可能报错。
  • 变量名不够明确。
  • 后续想加个输网页url在本地看小说功能,也就是去掉(黄色)小广告。
  • 然后把序列化存本地的行为改成存数据库,整个后台管理系统,记录看过的书的进度。或许可以学个fxml?
  • 当然不知道什么时候再写就是了

upd: 2022/5/5

  • 增加了解码种类,但utf-16LC似乎还是无法解码

根据txt文件头识别编码类型。RandomAccessFile默认编码为ISO-8859-1,因此用getbytes获得byte数组,再按其他编码类型转回String。

 res=new String(a.getBytes("ISO-8859-1"),charset);
  • 修复加载期间点击会死机的bug

使用JavaFX的Task类。

关于Task类等介绍

        Task<Void> task = new Task<>() {
                @Override
                protected Void call() throws Exception {
                    //子线程获取主线程变量
                    is_loading=1;
                    book = new Book(name, path);
                    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(output));
                    out.writeObject(book);
                    out.close();
                    is_loading=0;
                    Platform.runLater(new Runnable() {
                        @Override
                        public void run() {
                            showText();
                        }
                    });
                    return null;
                }
            };
            Thread thread = new Thread(task);
            thread.start();    

加载期间不允许目录等被点击。

用runLater指定线程结束后事件。如果用join锁住的话主线程不推进,还是会死机。

    •   接下来增加手动gc功能,加个识别后缀是否为txt,以及task执行过程中手动取消功能。

upd: 2022/5/7

  • 没有手动gc,改成翻页打开新书自动gc;System.gc()这个函数意思是告诉jvm可以gc了,具体gc看jvm心情。但一般还是会执行的。
  • 增加手动取消打开书本按钮。

判断当前线程是否被关闭:if(Thread.currentThread().isInterrupted())

但task执行完没找到能判断是否正常结束的方法,所以设了个flag。放在Platform.runLater

  • 修复了一些正则表达式瞎匹配的情况

upd: 2023/12/26

  • 推荐个手机阅读器,实现了本人所有梦想中的效果,此贴完结。
  • legado
  • 直接在github上下安装包就好