PySide6を使って作っています。
マークダウンにはコードの部分のみ対応し、そのほかの部分には対応していません。
PaintLabelBox:文字描画の最もコアな部分を担っています。
PaintButton:ボタンで今回はコピーボタンに使用しています。
PaintChatTitleBox:タイトルとコピーボタンのオブジェクトです。
PaintTitelAndContents:タイトルとコンテンツのオブジェクトです。
PaintChatNode:チャットノードです。ここでプログラムコードを認識して特別な表示にしています。
PaintChatThread:PaintChatNodeを配列管理して、チャットのスレッドにしています。
PaintArrowSlider:スライダーです。
ChatPaintWidget:PaintChatThread、PaintArrowSliderを統合しています。
ChatWidget:ChatPaintWidgetにタイトルや入力欄をチャットとして使えるようにしています。
AppendNodeEvent、AppendTextEvent、SetLastNodeTextイベントとして受け取るためのクラスです。スレッド建てて。これらで受け取らないと、すべての処理が終わるまで固まってしまいます。
import sys
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLabel, QPushButton, QSizePolicy
from PySide6.QtGui import QPainter, QColor, QFont, QFontMetrics, QPen, QBrush, QMouseEvent, QKeyEvent, QWheelEvent
from PySide6.QtCore import Qt, QRect, QPoint, QEvent, QCoreApplication ,Signal, QThread, QObject
import re
import pyperclip
from langchain_core.callbacks import BaseCallbackHandler
from typing import Any
import threading
class PaintLabelBox():
def __init__(self, text=""):
self.rect = QRect(0, 0, 150, 50)
self.font = QFont("Arial", 15)
self.span = 1
self.text = text
self.align = Qt.AlignLeft
self.height_fit = True
self.border = 5
self.__set_text_rect()
self.__fit_text()
def set_font_size(self, size):
self.font = QFont("Arial", size)
def __set_text_rect(self):
self.text_rect = QRect(
int(self.rect.x() + self.span+self.border),
int(self.rect.y() + self.span+self.border),
self.rect.width() - (self.span+self.border) * 2,
self.rect.height() - (self.span+self.border) * 2)
def set_hieght_fit(self, fit_flag):
self.height_fit = fit_flag
def set_size(self, width, hight):
if width < ((self.span+self.border) * 2)*2:
width = ((self.span+self.border) * 2)*2
if hight < ((self.span+self.border) * 2)*2:
hight = ((self.span+self.border) * 2)*2
self.rect.setRect(self.rect.x(), self.rect.y(), width, hight)
# self.__set_text_rect()
self.__fit_text()
def set_width(self, w):
if w < ((self.span+self.border) * 2)*2:
w = ((self.span+self.border) * 2)*2
self.rect.setRect(self.rect.x(), self.rect.y(), w, self.rect.height())
self.__set_text_rect()
self.__fit_text()
def set_position(self, x, y):
self.rect.setRect(x, y, self.rect.width(), self.rect.height())
self.__fit_text()
def move(self, x, y):
self.rect.setRect(self.rect.x() + x, self.rect.y() + y,
self.rect.width(), self.rect.height())
self.__set_text_rect()
def set_text(self, text):
self.text = text
self.__fit_text()
def get_text(self):
return self.text
def append_text(self, text):
self.text += text
self.__fit_text()
def width(self):
return self.rect.width()
def height(self):
return self.rect.height()
def x(self):
return self.rect.x()
def y(self):
return self.rect.y()
# テキストをwidthで改行する
def set_span(self, span):
self.span = span
def set_border(self, bsize):
self.border = bsize
def get_border(self):
return self.border
def __fit_text(self):
metrics = QFontMetrics(self.font)
lines = self.text.split("\n")
result_text = ""
for line in lines:
text_width = metrics.horizontalAdvance(line)
# 初めの大きさの確認で問題なければ何もしない。
if text_width < self.text_rect.width():
result_text += "\n" + line
continue
result_text += "\n" + self.__add_new_line(metrics, line)
self.fit_text = result_text[1:]
self.__fit_height()
def __add_new_line(self, metrics, line):
if len(line) <= 1:
return line
text_width = metrics.horizontalAdvance(line)
num = len(line)
# 分割位置の計算
sep_num = int(self.text_rect.width()/text_width * num)
line_buf = line[:sep_num]
text_width = metrics.horizontalAdvance(line_buf)
pre_text = line_buf
if text_width < self.text_rect.width():
# 分割後のサイズが小さいとき一文字ずつ追加して大きさを超えるところを探す。
while text_width < self.text_rect.width():
pre_text = line_buf
sep_num += 1
line_buf = line[:sep_num]
text_width = metrics.horizontalAdvance(line_buf)
# ここを抜けたという事は行き過ぎなので一つ戻す。
sep_num -= 1
pass
elif self.text_rect.width() < text_width:
# 分割後のサイズが大きいとき一文字減らして追加して大きさを以下になるところを探す。
while self.text_rect.width() < text_width:
sep_num -= 1
line_buf = line[:sep_num]
text_width = metrics.horizontalAdvance(line_buf)
pre_text = line_buf
pass
else:
pre_text = line_buf
# 同じとき完了
result_text = pre_text
# 残った文字列が表示領域より大きいとき再帰的の呼び出す。
line_buf = line[sep_num:]
text_width = metrics.horizontalAdvance(line_buf)
if self.text_rect.width() < text_width:
result_text += "\n" + self.__add_new_line(metrics, line_buf)
else:
result_text += "\n" + line_buf
return result_text
def __fit_height(self):
if self.height_fit:
metrics = QFontMetrics(self.font)
line_height = metrics.boundingRect(self.fit_text).height() # 行の高さ
line_num = len(self.fit_text.split("\n"))
self.rect = QRect(self.rect.x(), self.rect.y(),
self.rect.width(),
(line_height+metrics.leading())*line_num +
(self.span+self.border)*2)
self.__set_text_rect()
def set_align(self, pos_st):
if "left" == pos_st.lower():
self.align = Qt.AlignLeft
elif "center" == pos_st.lower():
self.align = Qt.AlignCenter
elif "right" == pos_st.lower():
self.align = Qt.AlignRight
def get_align(self):
return self.align
def draw(self, obj):
painter = QPainter(obj)
painter.setFont(self.font)
if 0 == self.border:
painter.setPen(QPen(QColor(255, 255, 255), self.border)) # 枠線
else:
painter.setPen(QPen(QColor(0, 0, 0), self.border)) # 枠線
painter.setBrush(QBrush(QColor(255, 255, 255))) # 塗りつぶし
painter.drawRect(self.rect)
painter.setPen(QPen(QColor(0, 0, 0), self.border)) # 枠線
painter.drawText(self.text_rect, self.align, self.fit_text)
class PaintButton(PaintLabelBox):
NO_STATE = 0
NOW_CLICKED = 1
NOW_HOVER = 2
def __init__(self, text=""):
super().__init__( text)
self.rect = QRect(50, 50, 100, 50)
self.state = self.NO_STATE
self.set_align("center")
self.nostate_color = QColor(192, 192, 192)
self.clicked_color = QColor(255, 0, 0)
self.hover_color = QColor(224, 224, 224)
def check_point_in(self, pos):
if self.rect.contains(pos):
return True
else:
return False
def mouse_down(self, fpos):
pos = QPoint(fpos.x(),fpos.y())
if self.rect.contains(pos):
self.state = self.NOW_CLICKED
return True
else:
return False
def mouse_up(self, fpos):
pos = QPoint(fpos.x(), fpos.y())
self.state = self.NO_STATE
def mouse_hover(self, fpos):
pos = QPoint(fpos.x(), fpos.y())
if self.rect.contains(pos):
if self.NO_STATE == self.state:
self.state = self.NOW_HOVER
else:
self.state = self.NO_STATE
def set_back_color(self, color):
self.nostate_color = color
def set_clicked_back_color(self, color):
self.clicked_color = color
def set_hover_back_color(self, color):
self.hover_color = color
def draw(self, obj):
painter = QPainter(obj)
if self.NO_STATE == self.state:
painter.setPen(QPen(QColor(0, 0, 0), 2)) # 青色の枠線
painter.setBrush(QBrush(self.nostate_color)) # 赤色の塗りつぶし
if self.NOW_CLICKED == self.state:
painter.setPen(QPen(QColor(0, 0, 0), 2)) # 青色の枠線
painter.setBrush(QBrush(self.clicked_color)) # 赤色の塗りつぶし
if self.NOW_HOVER == self.state:
painter.setPen(QPen(QColor(0, 0, 0), 2)) # 青色の枠線
painter.setBrush(QBrush(self.hover_color)) # 赤色の塗りつぶし
painter.drawRect(self.rect)
painter.setPen(QPen(QColor(0, 0, 0), 2)) # 青色の枠線
painter.drawText(self.rect, self.get_align(), self.text)
class PaintChatTitleBox():
def __init__(self, title_text):
self.button = PaintButton("copy")
self.title = PaintLabelBox(title_text)
def mouse_down(self, pos):
return self.button.mouse_down(pos)
def mouse_up(self, pos):
self.button.mouse_up(pos)
def mouse_hover(self, pos):
self.button.mouse_hover(pos)
def set_title_text(self, text):
self.title.set_text(text)
def get_title_text(self):
self.title.get_text()
def set_font_size(self, size):
if size < 7:
size = 7
self.title.set_font_size(size)
self.button.set_font_size(size-2)
def set_title_size(self, width, height):
self.title.set_size(width, height)
self.button.set_position(self.title.x() + self.title.width() -
self.button.width() - 1,
self.title.y() + 1)
def set_button_size(self, width, height):
self.button.set_size(width, height)
def set_position(self, x, y):
self.title.set_position(x, y)
self.button.set_position(x + self.title.width() -
self.button.width() - 1, y + 1)
def move(self, x, y):
self.title.move(x, y)
self.button.move(x, y)
def height(self):
return self.title.height()
def width(self):
return self.title.width()
def x(self):
return self.title.x()
def y(self):
return self.title.y()
def check_point_in(self, pos):
return self.button.check_point_in(pos)
def draw(self, obj):
self.title.draw(obj)
self.button.draw(obj)
class PaintTitelAndContents():
def __init__(self, title=" ", contents=""):
self.title = PaintChatTitleBox(title)
self.contents = PaintLabelBox(contents)
def mouse_down(self, pos):
return self.title.mouse_down(pos)
def mouse_up(self, pos):
self.title.mouse_up(pos)
def mouse_hover(self, pos):
self.title.mouse_hover(pos)
def set_contents_height_fit(self, fit_flag):
self.contents.set_hieght_fit(fit_flag)
def set_title(self, text):
self.title.set_title_text(text)
def get_title(self):
self.title.get_title_text()
def set_contents(self, text):
self.contents.set_text(text)
def get_contents_text(self):
return self.contents.get_text()
def append_text(self, text):
self.contents.append_text(text)
def set_title_font_size(self, size):
if size < 7:
size = 7
self.title.set_font_size(size)
def set_contents_font_size(self, size):
if size < 7:
size = 7
self.contents.set_font_size(size)
def set_contents_height(self, height):
self.contents.set_size(self.contents.width(), height)
def add_contents_height(self, height):
self.contents.set_size(self.contents.width(),
self.contents.height() + height)
def get_contents_height(self):
return self.contents.height()
def get_title_height(self):
return self.title.height()
def set_width(self, width):
self.title.set_title_size(width, self.title.height())
self.contents.set_size(width, self.contents.height())
self.contents.set_position(self.title.x(),
self.title.y() + self.title.height())
def set_position(self, x, y):
self.title.set_position(x, y)
self.contents.set_position(self.title.x(),
self.title.y() + self.title.height())
def set_contents_border(self, bsize):
self.contents.set_border(bsize)
def get_contents_border(self):
return self.contents.get_border()
def move(self, x, y):
self.title.move(x, y)
self.contents.move(x, y)
def height(self):
return self.title.height() + self.contents.height()
def width(self):
return self.title.width()
def x(self):
return self.title.x()
def y(self):
return self.title.y()
def check_point_in(self, pos):
return self.title.check_point_in(pos)
def draw(self, obj):
self.title.draw(obj)
self.contents.draw(obj)
class PaintChatNode():
NOMAL_BLOCK = 0
PROGRAM_BLOCK = 1
def __init__(self,
title=" ",
contents=" "):
self.main = PaintTitelAndContents(title, "")
self.blok_type = self.NOMAL_BLOCK
self.main.set_contents_height_fit(False)
self.contents_text = ""
self.contents = []
self.extract_code_block(contents)
def mouse_down(self, pos):
if self.main.mouse_down(pos):
pyperclip.copy(self.contents_text)
# print("self.main.get_text()", self.contents_text)
for obj in self.contents:
if PaintTitelAndContents is type(obj):
if obj.mouse_down(pos):
pyperclip.copy(obj.get_contents_text())
# print("obj.get_text()", obj.get_contents_text())
#copy
def mouse_up(self, pos):
self.main.mouse_up(pos)
for obj in self.contents:
if PaintTitelAndContents is type(obj):
obj.mouse_up(pos)
def mouse_hover(self, pos):
self.main.mouse_hover(pos)
def set_title(self, text):
self.main.set_title(text)
def set_contents(self, text):
self.contents = []
self.extract_code_block(text)
self.main.set_contents(text)
def append_text(self, text):
self.main.append_text(text)
text = self.main.get_contents_text()
self.contents = []
self.extract_code_block(text)
def set_position(self, x, y):
self.main.set_position(x, y)
for obj in self.contents:
if PaintTitelAndContents is type(obj):
self.__set_titelandcontents__width_and_position(obj)
else:
self.__set_label_width_and_position(obj)
self.__fit_height()
def __set_label_width_and_position(self, label):
label.set_width(self.width() - self.main.get_contents_border()*2)
# x位置の設定 yは後で合わせる
label.set_position(self.x() + self.main.get_contents_border(),
self.y() + self.main.get_contents_border())
def __set_titelandcontents__width_and_position(self, tc):
tc.set_width(self.width() - self.main.get_contents_border()*4)
# x位置の設定 yは後で合わせる
tc.set_position(self.x() + self.main.get_contents_border()*2,
self.y() + self.main.get_contents_border())
def set_width(self, width):
self.main.set_width(width)
for obj in self.contents:
if PaintTitelAndContents is type(obj):
self.__set_titelandcontents__width_and_position(obj)
else:
self.__set_label_width_and_position(obj)
self.__fit_height()
def set_contents_height(self, height):
self.main.set_contents_height(height)
def height(self):
return self.main.height()
def width(self):
return self.main.width()
def x(self):
return self.main.x()
def y(self):
return self.main.y()
def append_contents_ojbect(self, obj):
self.contents.append(obj)
if 0 == len(self.contents):
obj.set_position(self.main.x(), self.main.get_title_height())
self.main.add_contents_height(obj.height())
def extract_code_block(self, text): # コードブロックの抽出
self.contents_text = text
lines = text.split("\n")
program_name = ""
text_buf = ""
for line in lines:
""
line = line.rstrip()
if self.NOMAL_BLOCK == self.blok_type:
# nomal blockの時
# 正規表現でコードブロックを検索
match = re.search(r'^```(.*?)', line, re.DOTALL)
if match:
self.blok_type = self.PROGRAM_BLOCK
program_name = line[3:]
# ここまでのテキストデータをラベルとして追加
text_buf = text_buf.rstrip()
label = PaintLabelBox(text_buf)
label.set_border(0)
self.__set_label_width_and_position(label)
self.append_contents_ojbect(label)
# 初期化
self.last_text = text_buf
text_buf = ""
continue
else:
# purogram blockの時
match = re.search(r'```$', line, re.DOTALL) # 正規表現でコードブロックを検索
if match:
self.blok_type = self.NOMAL_BLOCK
# ここまでのテキストデータをプログラムとして追加
text_buf = text_buf.rstrip()
program = PaintTitelAndContents(program_name, text_buf)
# program = program.get_main_box()
self.__set_titelandcontents__width_and_position(program)
self.append_contents_ojbect(program)
# 初期化
program_name = ""
self.last_text = text_buf
text_buf = ""
continue
text_buf += line + "\n"
if self.PROGRAM_BLOCK == self.blok_type:
# プログラムブロックを終了する前に終了した
# program block
text_buf = text_buf.rstrip()
program = PaintTitelAndContents(program_name, text_buf)
self.__set_titelandcontents__width_and_position(program)
self.append_contents_ojbect(program)
self.blok_type = self.NOMAL_BLOCK
else:
# ノーマルブロックの状態で終了した。
# nomal block
text_buf = text_buf.rstrip()
label = PaintLabelBox(text_buf)
label.set_border(0)
self.__set_label_width_and_position(label)
self.append_contents_ojbect(label)
self.last_text = text_buf
self.__fit_height()
def __fit_height(self):
h = self.main.get_title_height()
count = 0
for obj in self.contents:
count += 1
obj.set_position(obj.x(),
self.y() + h +
self.main.get_contents_border() * count)
h += obj.height()
self.set_contents_height(h-self.main.get_title_height() +
self.main.get_contents_border()*count*2)
def draw(self, obj):
self.main.draw(obj)
for pobj in self.contents:
pobj.draw(obj)
class PaintChatThread():
def __init__(self):
self.nodes = []
self.draw_area = QRect(0, 0, 100, 1000)
self.draw_start_index = 0
self.node_space = 10
def mouse_down(self, pos):
pos = QPoint(self.draw_area.x() + pos.x(),
self.draw_area.y() + pos.y())
for node in self.nodes:
node.mouse_down(pos)
def mouse_up(self, pos):
pos = QPoint(self.draw_area.x() + pos.x(),
self.draw_area.y() + pos.y())
for node in self.nodes:
node.mouse_up(pos)
def mouse_hover(self, pos):
pos = QPoint(self.draw_area.x() + pos.x(),
self.draw_area.y() + pos.y())
for node in self.nodes:
node.mouse_hover(pos)
def append(self, node):
self.nodes.append(node)
if 1 < len(self.nodes):
# self.nodes[-1].set_position(
# self.nodes[-2].x(),
# self.nodes[-2].y() + self.nodes[-2].height() + self.node_space * (len(self.nodes)-1))
self.nodes[-1].set_position(
self.nodes[-2].x(),
self.nodes[-2].y() + self.nodes[-2].height() + self.node_space)
def append_node(self, title, contents):
node = PaintChatNode(title, contents)
if 0 < len(self.nodes):
n_bottom = self.nodes[-1].y() + self.nodes[-1].height()
da_bottom = self.draw_area_y() + self.draw_area_height()
width = self.width()
node.set_width(width)
self.append(node)
if 1 < len(self.nodes):
# print("n_bottom", n_bottom)
# print("da_bottom", self.draw_area_y() + self.draw_area_height())
if n_bottom == da_bottom:
self.set_draw_area_y(self.nodes[-1].y() + self.nodes[-1].height() - self.draw_area_height())
elif n_bottom <= da_bottom:
post_bottom = self.draw_area_y() + self.draw_area_height()
if da_bottom <= post_bottom:
self.set_draw_area_y(self.nodes[-1].y() + self.nodes[-1].height() - self.draw_area_height())
def append_text(self, text):
pre_draw_bottom = self.draw_area_y() + self.draw_area_height()
pre_contentts_height = self.height()
if 0 < len(self.nodes):
self.nodes[-1].append_text(text)
self.__set_draw_start_index()
if pre_draw_bottom == pre_contentts_height:
self.set_draw_area_y(self.height()-self.draw_area_height())
elif pre_contentts_height <= pre_draw_bottom:
post_bottom = self.draw_area_y() + self.draw_area_height()
if pre_draw_bottom <= post_bottom:
self.set_draw_area_y(self.height()-self.draw_area_height())
def set_last_node_text(self, text):
pre_draw_bottom = self.draw_area_y() + self.draw_area_height()
pre_contentts_height = self.height()
if 0 < len(self.nodes):
self.nodes[-1].set_contents(text)
self.__set_draw_start_index()
if pre_draw_bottom == pre_contentts_height:
self.set_draw_area_y(self.height()-self.draw_area_height())
def height(self):
if 0 < len(self.nodes):
return self.nodes[-1].y() + self.nodes[-1].height()
else:
return 0
def width(self):
if 0 < len(self.nodes):
return self.nodes[-1].width()
else:
return 0
def draw_area_height(self):
return self.draw_area.height()
def draw_area_y(self):
return self.draw_area.y()
def set_draw_area_y(self, y):
self.set_draw_area(self.draw_area.x(),
y,
self.draw_area.width(),
self.draw_area.height())
def set_draw_area(self, x, y, width, height):
pre_y = self.draw_area.y()
if y < 0:
y = 0
self.draw_area.setRect(x, y, width, height)
reverse = True
if pre_y < self.draw_area.y():
reverse = False
self.__set_draw_start_index(reverse)
def set_width(self, w):
y_deff_buf = -1
y_buf = -1
change_flag = True
for i in range(len(self.nodes)):
# 一番上の 描画 ノードとの関係を固定
if 0 == i:
y_deff_buf = self.draw_area.y() - self.nodes[i].y()
change_flag = True
self.nodes[i].set_width(w)
if 0 < i:
self.nodes[i].set_position(
self.nodes[i - 1].x(),
self.nodes[i - 1].y() + self.nodes[i-1].height() + self.node_space)
# 一番上の 描画 ノードとの関係を固定
if 0 <= self.draw_area.y() - self.nodes[i].y():
y_deff_buf = self.draw_area.y() - self.nodes[i].y()
change_flag = True
if change_flag:
y_buf = self.nodes[i].y()
change_flag = False
# 描画範囲を設定
if self.nodes[-1].y()+self.nodes[-1].height() - self.draw_area.height() < y_buf + y_deff_buf:
y_deff_buf = (self.nodes[-1].y()+self.nodes[-1].height() - self.draw_area.height())
y_buf = 0
if y_buf + y_deff_buf < 0:
y_buf = 0
y_deff_buf = 0
self.draw_area.setRect(
self.draw_area.x(),
y_buf + y_deff_buf,
w,
self.draw_area.height())
def move_vertical(self, y):
self.draw_area.setRect(
self.draw_area.x(),
self.draw_area.y() + y,
self.draw_area.width(),
self.draw_area.height())
self.__set_draw_start_index(y < 0)
def __set_draw_start_index(self, reverse=False):
if 0 < len(self.nodes):
if len(self.nodes) <= self.draw_start_index:
self.draw_start_index = len(self.nodes) -1
if reverse:
for i in range(self.draw_start_index, -1, -1):
if self.draw_area.y() < self.nodes[i].y() +self.nodes[i].height():
if self.nodes[i].y() < self.draw_area.y() + self.draw_area.height():
self.draw_start_index = i
if self.nodes[i].y() + self.nodes[i].height() < self.draw_area.y():
break
else:
for i in range(self.draw_start_index, len(self.nodes)):
if self.draw_area.y() < self.nodes[i].y() + self.nodes[i].height():
if self.nodes[i].y() < self.draw_area.y() + self.draw_area.height():
self.draw_start_index = i
return
def draw(self, obj):
if 0 < len(self.nodes):
for i in range(self.draw_start_index, len(self.nodes)):
if self.draw_area.y() < self.nodes[i].y() + self.nodes[i].height():
if self.nodes[i].y() < self.draw_area.y() + self.draw_area.height():
# draw area内のノードだけ描画
pre_x = self.nodes[i].x()
pre_y = self.nodes[i].y()
self.nodes[i].set_position(self.nodes[i].x()-self.draw_area.x(),
self.nodes[i].y()-self.draw_area.y())
self.nodes[i].draw(obj)
self.nodes[i].set_position(pre_x,
pre_y)
if self.draw_area.y() + self.draw_area.height() < self.nodes[i].y():
return
def get_draw_bottom(self):
return self.draw_area.x() + self.draw_area.height()
class PaintArrowSlider():
NO_STATE = 0
CLICK_UP = 1
CLICK_DOWN = 2
CLICK_SLIDER = 3
CLICK_SLIDER_UP = 4
CLICK_SLIDER_DOWN = 5
def __init__(self):
self.up_button = PaintButton("▲")
self.down_button = PaintButton("▼")
self.slider = PaintButton(" ")
self.slider.set_hieght_fit(False)
self.state = self.NO_STATE
self.slider_min = 10
self.rect = QRect(0, 0, 10, 100)
self.border = 0
self.y_position_ratio = 1
self.slider_height_ratio = 1
self.set_up_down_button_size(30, 30)
self.slider.set_width(30)
self.set_slider_height(1)
self.set_slider_position(0)
self.pre_mouse_position = QPoint(0, 0)
self.slider.set_clicked_back_color(QColor(224,224,224))
def x(self):
return self.up_button.x()
def y(self):
return self.up_button.y()
def width(self):
return self.up_button.width()
def height(self):
return self.rect.height()
def set_up_down_button_size(self, width, height):
now_height = self.height()
self.rect = QRect(self.rect.x(),
self.rect.y(),
width,
self.rect.height())
self.up_button.set_size(width, height)
self.down_button.set_size(width, height)
if now_height < height * 3:
now_height = height*3
self.set_height(now_height)
def set_slider_position(self, pos_ratio):
if 1 < pos_ratio:
pos_ratio = 1
if pos_ratio < 0:
pos_ratio = 0
self.y_position_ratio = pos_ratio
self.slider.set_position(
self.slider.x(),
self.up_button.height() + (self.down_button.y() -
(self.up_button.y() +
self.up_button.height() +
self.slider.height()))
* self.y_position_ratio)
def set_position(self, x, y):
self.up_button.set_position(x, y)
# self.slider.set_position(x, y)
self.down_button.set_position(
x,
self.rect.height() - self.down_button.height())
self.slider.set_position(x, y)
self.set_slider_position(self.y_position_ratio)
self.rect = QRect(x, y, self.rect.width(), self.rect.height())
def mouse_down(self, pos):
pos = QPoint(pos.x(),pos.y())
if self.up_button.mouse_down(pos):
self.state = self.CLICK_UP
elif self.down_button.mouse_down(pos):
self.state = self.CLICK_DOWN
elif self.slider.mouse_down(pos):
self.state = self.CLICK_SLIDER
self.pre_mouse_position = pos
elif self.rect.contains(pos):
if pos.y() < self.slider.y():
self.state = self.CLICK_SLIDER_UP
elif self.slider.y() + self.slider.height() < pos.y():
self.state = self.CLICK_SLIDER_DOWN
return self.state
def mouse_up(self, pos):
self.state = self.NO_STATE
self.up_button.mouse_up(pos)
self.down_button.mouse_up(pos)
self.slider.mouse_up(pos)
def mouse_hover(self, pos):
self.up_button.mouse_hover(pos)
self.down_button.mouse_hover(pos)
self.slider.mouse_hover(pos)
def mouse_move(self, pos):
vm = QPoint(int(pos.x() - self.pre_mouse_position.x()),
int(pos.y() - self.pre_mouse_position.y()))
if self.state == self.CLICK_SLIDER:
if vm.y() < 0:
if self.up_button.y() + self.up_button.height() <\
self.slider.y() + vm.y():
self.slider.set_position(
self.slider.x(),
self.slider.y() + vm.y())
else:
self.slider.set_position(
self.slider.x(),
self.up_button.y() + self.up_button.height())
else:
if self.slider.y() + self.slider.height() + vm.y() <\
self.down_button.y():
self.slider.set_position(
self.slider.x(),
self.slider.y() + vm.y())
else:
self.slider.set_position(
self.slider.x(),
self.down_button.y()-self.slider.height())
self.pre_mouse_position = pos
buf = self.slider.y()-(self.up_button.y()+self.up_button.height() )
self.y_position_ratio = (
buf/
(self.down_button.y() -
(self.slider.height() +
self.up_button.y() +
self.up_button.height()))
)
return vm
def get_state(self):
return self.state
def set_width(self, width):
self.rect = QRect(self.rect.x(), self.rect.y(),
width, self.rect.height())
self.up_button.set_size(width, self.up_button.height())
self.down_button.set_size(width, self.down_button.height())
self.slider.set_size(width, self.slider.height())
def set_height(self, height):
self.rect = QRect(self.rect.x(), self.rect.y(),
self.rect.width(), height)
ratio = self.get_slider_ratio()
self.down_button.set_position(self.down_button.x(),
height-self.down_button.height())
slider_area_height = self.down_button.y() - (self.up_button.height() + self.up_button.y())
self.slider.set_size(self.slider.width(), int(slider_area_height*ratio))
self.set_slider_position(self.y_position_ratio)
def set_rect(self, x , y, width, height):
self.set_position(x, y)
self.set_width(width)
self.set_height(height)
def get_rect(self):
return self.rect
def get_slider_position_ratio(self):
return self.y_position_ratio
def get_slider_ratio(self):
s = self.up_button.y() + self.up_button.height() - self.down_button.y()
return s / self.slider.height()
def set_slider_height(self, ratio):
if 1 <= ratio:
ratio = 1
s = self.down_button.y() - (self.up_button.y() + self.up_button.height())
s = s * ratio
self.slider.set_size(self.slider.width(), int(s))
if int(s) < self.slider_min:
self.slider.set_size(self.slider.width(), self.slider_min)
self.slider_height_ratio = ratio
def __draw_back(self, obj):
# スライダー部分の背景
painter = QPainter(obj)
painter.setPen(QPen(QColor(128, 128, 128), self.border)) # 枠線
painter.setBrush(QBrush(QColor(128, 128, 128))) # 塗りつぶし
rect = QRect(
self.up_button.x() + 1,
self.up_button.y()+self.up_button.height(),
self.up_button.width() - 4,
self.down_button.y() - (self.up_button.y() + self.up_button.height()))
# ボタン類の描画
painter.drawRect(rect)
def draw(self, obj):
self.__draw_back(obj)
self.up_button.draw(obj)
self.down_button.draw(obj)
self.slider.draw(obj)
class ChatPaintWidget(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Graphics Text with Size and Newline - Debug Version")
self.font = QFont("Arial", 50)
self.name = QLabel("")
self.name.setMaximumWidth(1500)
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.name)
self.setLayout(self.layout)
self.chat_thread = PaintChatThread()
self.slider = PaintArrowSlider()
self.update_font_size(20)
self.chat_thread.append_node(f"ai", "作業指示をしてください。") #これがないとウィンドが表示されず終了してしまう。
# テスト用
# for i in range(100):
# self.chat_thread.append_node(f"user1_{i}", "testデータです。aaa1aaaa2aaaaaa3aaaaa4aaa5aaaaaa6aaaaa7aaaa8aa")
# self.chat_thread.append_node(f"ai1_{i}", "わかりました。\n```python\nprint('test')\na=2\nb=4\n```\n 終わりです。sssssssssssssssssssssssssssssssssssssssssssssssssssssss")
# self.chat_thread.append_node(f"user2_{i}", "どうなっているの?\nok")
def paintEvent(self, event):
self.chat_thread.draw(self)
self.slider.draw(self)
def __page_up(self, deff):
self.chat_thread.move_vertical(-deff)
if self.chat_thread.draw_area_y() < 0:
self.chat_thread.set_draw_area_y(0)
self.slider.set_slider_position(self.chat_thread.draw_area_y()/(self.chat_thread.height() - self.chat_thread.draw_area_height()))
def __page_down(self, deff):
self.chat_thread.move_vertical(deff)
if self.chat_thread.height() < self.chat_thread.draw_area_y() + self.chat_thread.draw_area_height():
self.chat_thread.set_draw_area_y(
self.chat_thread.height() - self.chat_thread.draw_area_height())
self.slider.set_slider_position(self.chat_thread.draw_area_y()/(self.chat_thread.height() - self.chat_thread.draw_area_height()))
def mousePressEvent(self, event: QMouseEvent):
self.chat_thread.mouse_down(event.position())
res = self.slider.mouse_down(event.position())
dh = self.chat_thread.draw_area_height()
if res == self.slider.CLICK_UP:
self.__page_up(int(dh/10))
elif res == self.slider.CLICK_DOWN:
self.__page_down(int(dh/10))
elif res == self.slider.CLICK_SLIDER_UP:
self.__page_up(int(dh/10*9))
elif res == self.slider.CLICK_SLIDER_DOWN:
self.__page_down(int(dh/10*9))
self.chat_thread.height()
self.chat_thread.get_draw_bottom()
self.update()
def mouseReleaseEvent(self, event: QMouseEvent):
self.chat_thread.mouse_up(event.position())
self.slider.mouse_up(event.position())
self.update()
def mouseMoveEvent(self, event: QMouseEvent):
self.chat_thread.mouse_hover(event.position())
self.slider.mouse_hover(event.position())
self.slider.mouse_move(event.position())
res = self.slider.get_state()
if res == self.slider.CLICK_SLIDER:
pr = self.slider.get_slider_position_ratio()
self.chat_thread.set_draw_area_y(int((self.chat_thread.height() - self.chat_thread.draw_area_height())*pr))
self.update()
def keyPressEvent(self, event: QKeyEvent):
key = event.key()
dh = self.chat_thread.draw_area_height()
if key == Qt.Key_PageUp:
self.__page_up(int(dh/10*9))
elif key == Qt.Key_PageDown:
self.__page_down(int(dh/10*9))
elif key == Qt.Key_Up:
self.__page_up(int(dh/10))
# 上矢印キーが押されたときの処理
elif key == Qt.Key_Down:
self.__page_down(int(dh/10))
self.chat_thread.height()
self.chat_thread.get_draw_bottom()
self.update()
def wheelEvent(self, event: QWheelEvent):
# event.angleDelta().y() は、垂直方向の回転量を返します。
# 正の値は上方向への回転、負の値は下方向への回転を示します。
delta = event.angleDelta().y()
dh = self.chat_thread.draw_area_height()
# 回転量に応じて処理を行います。
if delta > 0:
# 上方向への回転時の処理
self.__page_up(int(dh/10))
else:
self.__page_down(int(dh/10))
# 下方向への回転時の処理
self.chat_thread.height()
self.chat_thread.get_draw_bottom()
self.update()
def update_font_size(self, font_size=None):
if font_size is None:
font_size = self.slider.value()
self.font.setPointSize(font_size)
self.update()
def __update_slider(self):
self.slider.set_height(self.height())
self.slider.set_position(self.width()-self.slider.width(), 0)
self.slider.set_slider_height(self.chat_thread.draw_area_height()/self.chat_thread.height())
self.slider.get_slider_position_ratio()
self.slider.set_slider_position(self.chat_thread.draw_area_y()/(self.chat_thread.height() - self.chat_thread.draw_area_height()))
self.slider.get_slider_ratio()
def resizeEvent(self, event):
da_y = self.chat_thread.draw_area_y()
if self.chat_thread.height() < self.height():
da_y = 0
else:
if self.chat_thread.height() <= da_y + self.height():
da_y = self.chat_thread.height() - self.height()
elif self.chat_thread.height() <= da_y + self.chat_thread.draw_area_height():
da_y = self.chat_thread.height() - self.height()
self.chat_thread.height()
self.chat_thread.set_draw_area(
-10, da_y, self.width()-self.slider.width() - 10, self.height())
if self.chat_thread.width() != self.width()-self.slider.width()-20:
self.chat_thread.set_width(self.width()-self.slider.width()-20)
self.__update_slider()
self.update()
def append_node(self, title, text):
self.chat_thread.append_node(title, text)
self.__update_slider()
self.update()
def append_text(self, text):
self.chat_thread.append_text(text)
self.__update_slider()
self.update()
def set_last_node_text(self, text):
self.chat_thread.set_last_node_text(text)
self.__update_slider()
self.update()
class ChatWidget(QWidget):
update_signal = Signal(str, str) # title, text用
def __init__(self, agent):
super().__init__()
self.chat_thread = ChatPaintWidget()
# title
self.title = QLabel("PaintGUI\r\n")
self.title.setFixedHeight(30)
self.title.setFont(QFont("Arial", 30))
self.title.setAlignment( Qt.AlignCenter)
self.sep1 = QLabel("-------------------------------------------------------------------------------------------------")
self.sep1.setFixedHeight(20)
self.sep1.setAlignment(Qt.AlignCenter)
font = QFont("Arial", 20)
font.setBold(True) # 太字に設定
self.sep1.setFont(font)
self.sep2 = QLabel("-------------------------------------------------------------------------------------------------")
self.sep2.setFixedHeight(20)
self.sep2.setAlignment(Qt.AlignCenter)
self.sep2.setFont(font)
# input 入力欄
self.input_edit = QTextEdit()
self.input_edit.setMaximumHeight(100)
self.send_button = QPushButton("Send")
self.send_button.clicked.connect(self.send_message)
# Layout for input area
input_layout = QHBoxLayout()
input_layout.addWidget(self.input_edit)
input_layout.addWidget(self.send_button)
# Main layout
main_layout = QVBoxLayout(self)
main_layout.addWidget(self.title)
main_layout.addWidget(self.sep1)
main_layout.addWidget(self.chat_thread)
main_layout.addWidget(self.sep2)
main_layout.addLayout(input_layout)
self.update_signal.connect(self._append_node_handler)
self.agent = agent
self.thread = None
def send_message(self):
message = self.input_edit.toPlainText()
if message:
#############################################################
self.chat_thread.append_node("user", message)
self.input_edit.clear()
# ワーカースレッドを作成して処理を移す
self.thread = QThread()
self.worker = AppendNodeWorker(self.agent, message)
self.worker.moveToThread(self.thread)
# スレッドが開始したらワーカーのrunを実行
self.thread.started.connect(self.worker.run)
# 処理が終わったら結果を受け取る
self.worker.finished.connect(self.on_response_received)
# スレッド終了時にスレッドを削除
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
# スレッド開始
self.thread.start()
def on_response_received(self, response):
# レスポンスを受け取って表示
#self.append_node("agent", response)
pass
def append_node(self, title, text):
#self.chat_thread.append_node(title, text)
self.update_signal.emit(title, text)
# QCoreApplication.postEvent(self, AppendNodeEvent(title, text))
def _append_node_handler(self, title, text):
# 実際の更新処理
self.chat_thread.append_node(title, text)
self.update() # QWidgetのupdateメソッド
QApplication.processEvents() # イベント即時処理
def update(self):
super().update()
QApplication.processEvents() # イベントを即座に処理
def append_text(self, text):
# self.chat_thread.append_text(text)
# self.update()
QCoreApplication.postEvent(self, AppendTextEvent(text))
def set_last_node_text(self, text):
# self.chat_thread.set_last_node_text(text)
QCoreApplication.postEvent(self, SetLastNodeText(text))
self.update()
def set_agent(self, agent):
self.agent = agent
# self.update()
def customEvent(self, event):
if event.type() == AppendNodeEvent.EventType:
self.chat_thread.append_node(event.name, event.text)
self.update()
elif event.type() == AppendTextEvent.EventType:
self.chat_thread.append_text(event.text)
self.update()
elif event.type() == SetLastNodeText.EventType:
self.chat_thread.set_last_node_text(event.text)
self.update()
class AppendNodeWorker(QObject):
finished = Signal(str) # 処理が終わったときに結果を送信するシグナル
def __init__(self, agent, message):
super().__init__()
self.agent = agent
self.message = message
def run(self):
# 重い処理をここで行う
response = self.agent.get_respons(self.message)
self.finished.emit(response) # 結果をシグナルで送信
class AppendNodeEvent(QEvent):
# EventType = QEvent.registerEventType()
EventType = QEvent.Type(QEvent.registerEventType()) # Use QEvent.Type()
def __init__(self, name, text):
super().__init__(AppendNodeEvent.EventType)
self.name = name
self.text = text
class AppendTextEvent(QEvent):
# EventType = QEvent.registerEventType()
EventType = QEvent.Type(QEvent.registerEventType()) # Use QEvent.Type()
def __init__(self, text):
super().__init__(AppendTextEvent.EventType)
self.text = text
class SetLastNodeText(QEvent):
# EventType = QEvent.registerEventType()
EventType = QEvent.Type(QEvent.registerEventType()) # Use QEvent.Type()
def __init__(self, text):
super().__init__(SetLastNodeText.EventType)
self.text = text
class MyStreamlitCallbackHandler(BaseCallbackHandler):
def __init__(self, container):
self.container = container
def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
self.container.append_text(token)
# super().on_llm_new_token(token, kwargs)
g_app = QApplication(sys.argv)
g_main_widget = QWidget()
g_main_layout = QVBoxLayout()
# main_layout = QHBoxLayout(main_widget)
g_my_widget = ChatWidget(None)
g_my_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # 重要な変更点
g_my_widget.setMinimumHeight(400)
g_my_widget.setFixedWidth(1000)
# スライダーとラベルのためのウィジェット
g_main_layout.addWidget(g_my_widget)
g_main_layout.addStretch(0)
g_my_widget.setLayout(g_main_layout)
def excute(agent):
global g_my_widget
global g_app
g_my_widget.set_agent(agent)
g_my_widget.show()
sys.exit(g_app.exec())
def set_agent(agent):
g_my_widget.set_agent(agent)
def update():
g_my_widget.update()
def append_node(name,text):
g_my_widget.append_node(name, text)
def set_last_node_text(text):
g_my_widget.set_last_node_text( text)
def get_stc_handler():
global g_my_widget
return MyStreamlitCallbackHandler(g_my_widget)
def get_mywidget():
return g_my_widget