精彩纷呈 发表于 2023-9-30 00:42:20

【日常收支账本】【Day03】完成编辑账本界面的新增动账记录功能——通过Ele

一、项目地址

https://github.com/LinFeng-BingYi/DailyAccountBook
二、新增

1. 解析xml文件

1.1 功能详述

解析所设计的xml文件格式,并将所得数据存入变量。
→→→点击查看xml格式←←←<DailyAccountBook>
    <balance>
      <fund>
            <value>5000.00</value>
            <category>0</category>
            <fundName>微信零钱</fundName>
      </fund>
      <fund>
            <value>999.00</value>
            <category>1</category>
            <fundName>中国银行卡</fundName>
      </fund>
      <fund>
            <value>90.00</value>
            <category>2</category>
            <fundName>羊城通</fundName>
      </fund>
      <fund>
            <value>1700.07</value>
            <category>3</category>
            <fundName>支付宝余额宝</fundName>
      </fund>
      <fund>
            <value>5000.00</value>
            <category>4</category>
            <fundName>代管存款</fundName>
      </fund>
    </balance>
    <year value="2023">
      <month value="09">
            <day value="11">
                <expenses>
                  <expense necessity="True" associatedFund="None">
                        <value>5.00</value>
                        <category>1</category>
                        <detail>地铁</detail>
                        <describe>早上上班。羊城通卡余额=100-5=95元</describe>
                        <from>2</from>
                  </expense>
                  <expense necessity="True" associatedFund="None">
                        <value>5.00</value>
                        <category>1</category>
                        <detail>地铁</detail>
                        <describe>晚上下班。羊城通卡余额=95-5=90元</describe>
                        <from>2</from>
                  </expense>
                  <expense necessity="False" associatedFund="None">
                        <value>1.00</value>
                        <category>12</category>
                        <detail>业务手续费</detail>
                        <describe>微信零钱提现1000的手续费</describe>
                        <from>0</from>
                  </expense>
                </expenses>
                <incomes>
                  <income associatedFund="4">
                        <value>3000.00</value>
                        <category>5</category>
                        
                        <detail>转账</detail>
                        <describe>托管人转给本人3000,帮忙存放。本人微信零钱余额=3000+3000=6000元;同时代管存款余额=2000+3000=5000元</describe>
                        <to>0</to>
                  </income>
                  <income associatedFund="None">
                        <value>0.07</value>
                        <category>2</category>
                        <detail>理财</detail>
                        <describe>昨日余额宝收益。余额=1700+0.07=1700.07</describe>
                        <to>3</to>
                  </income>
                </incomes>
                <movements>
                  
                  <movement>
                        <value>999.00</value>
                        <detail>提现</detail>
                        <describe>从微信零钱向中国银行卡提现1000元。完成后微信零钱余额=6000-999-1=5000;中国银行卡余额=0+999=999;被收取0.1%的手续费</describe>
                        <from>0</from>
                        <to>1</to>
                  </movement>
                </movements>
                <variation>
                  <fund>
                        <category>0</category>
                        <out>1.00</out>
                        <in>3000.00</in>
                  </fund>
                  <fund>
                        <category>1</category>
                        <out>0.00</out>
                        <in>0.00</in>
                  </fund>
                  <fund>
                        <category>2</category>
                        <out>10.00</out>
                        <in>0.00</in>
                  </fund>
                  <fund>
                        <category>3</category>
                        <out>0.00</out>
                        <in>0.07</in>
                  </fund>
                  <fund>
                        <category>4</category>
                        <out>0.00</out>
                        <in>3000.00</in>
                  </fund>
                </variation>
            </day>
      </month>
    </year>
</DailyAccountBook>解析目的:

[*]将balance元素中各项fund子元素存入列表,列表中每一项都是一个代表fund元素的字典;
[*]将对应日期的day元素中各种动账类型记录集合存入字典,该字典内容格式如下:
day_dict = {
    'expenses': ,
    'incomes': ,
    'movements': ,
    'variation':
}1.2 代码实现

解析balance元素:
    def parseBalance(self):
      e_balance = self.e_dailyAccountBook.find(".//balance")
      balance_list = []
      for e_fund in list(e_balance):
            balance_dict = {"value": float(e_fund.find('.//value').text),
                            "category": int(e_fund.find('.//category').text),
                            "fundName": e_fund.find('.//fundName').text}
            balance_list.append(balance_dict)
      return balance_list解析day元素:
    def getSpecificDateElement(self, date_str):
      """
      Describe: 根据日期字符串获取指定的day元素
      Args:
            date_str: str
                格式为"yyyyMMdd"
      Returns:
            若找到指定日期的元素,则返回Element类型的day元素.
            若未找到指定year,则返回int类型的0;若未找到指定month,则返回int类型的1;若未找到指定day,则返回int类型的2。
            int型返回值用于控制从何处开始初始化日期元素。
      """
      e_year = self.e_dailyAccountBook.find(".//year[@value='{}']".format(date_str[:4]))
      if e_year is None:
            return 0
      e_month = e_year.find(".//month[@value='{}']".format(date_str))
      if e_month is None:
            return 1
      e_day = e_month.find(".//day[@value='{}']".format(date_str))
      if e_day is None:
            return 2
      return e_day

    def parseSpecificDateElement(self, date_str):
      e_date = self.getSpecificDateElement(date_str)
      if isinstance(e_date, int):
            print("未找到这一天的数据!")
            return None

      parse_dict = dict()
      for child_node in list(e_date):
            print(child_node.tag)
            if child_node.tag == 'expenses':
                e_expenses = e_date.find(".//expenses")
                expenses_list =
                parse_dict['expenses'] = expenses_list
            elif child_node.tag == 'incomes':
                e_incomes = e_date.find(".//incomes")
                incomes_list =
                parse_dict['incomes'] = incomes_list
            elif child_node.tag == 'movements':
                e_movements = e_date.find(".//movements")
                movements_list =
                parse_dict['movements'] = movements_list
            elif child_node.tag == 'variation':
                e_variation = e_date.find(".//variation")
                variation_list =
                parse_dict['variation'] = variation_list
            else:
                print("未知类型的节点名")

      return parse_dict

    def parseExpense(self, e_expense):
      expense_dict = {
            'necessity': True if (e_expense.attrib['necessity'].lower() == 'true') else False,
            'value': float(e_expense.find('.//value').text),
            'category': int(e_expense.find('.//category').text),
            'detail': e_expense.find('.//detail').text,
            'describe': e_expense.find('.//describe').text,
            'from': int(e_expense.find('.//from').text),
            'associatedFund': int(e_expense.attrib['associatedFund']) if (
                  ('associatedFund' in e_expense.attrib) and e_expense.attrib['associatedFund'] != 'None') else None
      }

      return expense_dict

    def parseIncome(self, e_income):
      pass

    def parseMovement(self, e_movement):
      pass

    def parseVariation(self, e_fund):
      pass2. 编辑账本界面-新增行

2.1 功能详述

在选择文件或不同的日期后,解析xml文件中对应日期的收支记录,将其展示在QTableWidget中。此外,表格中最后一列增加两种操作控件:

[*]对于已存在记录的行:包含修改、删除按钮;
[*]对于空白行:包含新增按钮。点击新增后,该行变成已存在记录的行,故而操作控件也相应地变化,同时表格再新增一行空白行
2.2 代码实现

    def responseSelectedDateChanging(self):
      if not self.lineEdit_file_path.text():
            print("还未选择文件!")
            return
      self.file_processor = AccountBookXMLProcessor(self.lineEdit_file_path.text())
      self.file_parse_result = self.file_processor.parseSpecificDateElement(self.dateEdit.text().replace('/', ''))
      print(self.file_parse_result)
      if self.file_parse_result is None:
            self.file_parse_result = {}

      if 'expenses' not in self.file_parse_result:
            self.file_parse_result['expenses'] = []
      if 'incomes' not in self.file_parse_result:
            self.file_parse_result['incomes'] = []
      if 'movements' not in self.file_parse_result:
            self.file_parse_result['movements'] = []
      if 'variation' not in self.file_parse_result:
            self.file_parse_result['variation'] = []

      self.updateExpenseTable(self.file_parse_result['expenses'])
      self.updateIncomeTable(self.file_parse_result['incomes'])
      self.updateMovementTable(self.file_parse_result['movements'])

    def updateExpenseTable(self, expenses_list):
      self.tableWidget_expense.setRowCount(0)
      self.tableWidget_expense.setRowCount(len(expenses_list)+1)

      current_row = 0
      for expense_dict in expenses_list:
            self.tableWidget_expense.setItem(current_row, 0, QTableWidgetItem(str(expense_dict['necessity'])))
            self.tableWidget_expense.setItem(current_row, 1, QTableWidgetItem(str(expense_dict['value'])))
            self.tableWidget_expense.setItem(current_row, 2, QTableWidgetItem(str(expense_dict['category'])))
            self.tableWidget_expense.setItem(current_row, 3, QTableWidgetItem(str(expense_dict['detail'])))
            self.tableWidget_expense.setItem(current_row, 4, QTableWidgetItem(str(expense_dict['describe'])))
            self.tableWidget_expense.setItem(current_row, 5, QTableWidgetItem(str(expense_dict['from'])))
            self.tableWidget_expense.setItem(current_row, 6, QTableWidgetItem(str(expense_dict['associatedFund'])))
            self.tableWidget_expense.setCellWidget(current_row, 7, self.buttonsForExistRow(self.tableWidget_expense))
            current_row += 1

      self.tableWidget_expense.setItem(current_row, 4, QTableWidgetItem(' '))
      self.tableWidget_expense.setCellWidget(current_row, 7, self.buttonsForNewRow(self.tableWidget_expense))

    def updateIncomeTable(self, incomes_list):
      pass

    def updateMovementTable(self, movements_list):
      pass

    def buttonsForExistRow(self, tableWidget):
      widget = QWidget()
      # 更新
      updateBtn = QPushButton('更新')
      updateBtn.clicked.connect(lambda: self.updateTableRow(tableWidget))
      # 删除
      deleteBtn = QPushButton('删除')
      deleteBtn.clicked.connect(lambda: self.deleteTableRow(tableWidget))

      hLayout = QHBoxLayout(widget)
      hLayout.addWidget(updateBtn)
      hLayout.addWidget(deleteBtn)
      hLayout.setContentsMargins(5, 2, 5, 2)
      return widget

    def buttonsForNewRow(self, tableWidget):
      widget = QWidget()
      # 新增
      newBtn = QPushButton('新增')
      newBtn.clicked.connect(lambda: self.newTableRow(newBtn, tableWidget))

      hLayout = QHBoxLayout(widget)
      hLayout.addWidget(newBtn)
      hLayout.setContentsMargins(5, 2, 5, 2)
      return widget

    def updateTableRow(self, toggledBtn, tableWidget):
      pass

    def deleteTableRow(self, toggledBtn, tableWidget):
      pass

    def newTableRow(self, toggledBtn, tableWidget):
      print('触发了新增按钮')
      # 获取触发信号的控件所在行号
      row = tableWidget.indexAt(toggledBtn.parent().pos()).row()
      new_data_dict = dict()
      if tableWidget == self.tableWidget_expense:
            current_column_head = TABLEWIDGET_EXPENSE_COLUMN_HEAD
      elif tableWidget == self.tableWidget_income:
            current_column_head = TABLEWIDGET_INCOME_COLUMN_HEAD
      elif tableWidget == self.tableWidget_movement:
            current_column_head = TABLEWIDGET_MOVEMENT_COLUMN_HEAD
      else:
            print('未知控件触发新增按钮!')
            return
      # 用新增行数据构建字典
      for i in range(tableWidget.columnCount()-1):
            new_data_dict] = tableWidget.item(row, i).text()
      print(new_data_dict)

      # 插入新空行
      insert_pos = tableWidget.rowCount()
      tableWidget.insertRow(insert_pos)
      # 新空行"操作"列初始化按钮
      tableWidget.setCellWidget(insert_pos, tableWidget.columnCount()-1, self.buttonsForNewRow(tableWidget))
      # 新增行"操作"列初始化按钮
      tableWidget.setCellWidget(insert_pos-1, tableWidget.columnCount()-1, self.buttonsForExistRow(tableWidget))

      if tableWidget == self.tableWidget_expense:
            # 将"描述"字段预置空格
            tableWidget.setItem(insert_pos, 4, QTableWidgetItem(' '))
            # 用新增行的数据组织文件结构
            self.file_processor.organizeExpense(new_data_dict, self.dateEdit.text().replace('/', ''))
      elif tableWidget == self.tableWidget_income:
            tableWidget.setItem(insert_pos, 3, QTableWidgetItem(' '))
            self.file_processor.organizeIncome(new_data_dict, self.dateEdit.text().replace('/', ''))
      elif tableWidget == self.tableWidget_movement:
            tableWidget.setItem(insert_pos, 2, QTableWidgetItem(' '))
            self.file_processor.organizeMovement(new_data_dict, self.dateEdit.text().replace('/', ''))
      # 将结果文件暂时存放在工作目录
      self.file_processor.writeXMLFile(self.cwd+'\\AccountBookXMLFile.xml')3. 将新增行写入xml文件

3.1 功能详述

用从编辑账本界面获取的新增行字典,写入xml文件。具体过程如下:

[*]判断当前日期是否存在,四种情况:存在年/月/日、只存在年/月、只存在年、均不存在。判断方法:在方法getSpecificDateElement(date_str)中,通过int型返回值作为标志,再结合方法switch_caseInitStartDate(init_start, date_str)确定日期元素新增起始点,即判断从年/月/日开始新增。
[*]根据新增行字典内容,修改各存款账户的余额
[*]根据新增行字典内容,新增对应动账记录
[*]若收支记录关联了其他存款账户,则同时修改关联账户
3.2 代码实现

    def createChildElement(self, e_parent, child_name, child_text, chile_attr=None):
      if chile_attr is None:
            chile_attr = {}
      e_child = et.SubElement(e_parent, child_name, attrib=chile_attr)
      e_child.text = child_text
      return e_child

    def switch_caseInitStartDate(self, init_start, date_str):
      # 模拟C++中switch-case控制语句,不使用break的情况
      if init_start == 0:
            self.createChildElement(self.e_dailyAccountBook, 'year', None, {'value': date_str[:4]})
            init_start += 1
      if init_start == 1:
            e_year = self.e_dailyAccountBook.find(".//year[@value='{}']".format(date_str[:4]))
            self.createChildElement(e_year, 'month', None, {'value': date_str})
      #   init_start += 1
      # if init_start == 2:
      e_year = self.e_dailyAccountBook.find(".//year[@value='{}']".format(date_str[:4]))
      e_month = e_year.find(".//month[@value='{}']".format(date_str))
      e_date = self.createChildElement(e_month, 'day', None, {'value': date_str})
      return e_date

    def organizeExpense(self, expense_dict: dict, date_str):
      e_date = self.getSpecificDateElement(date_str)

      if isinstance(e_date, int):
            print("未找到这一天的数据!")

            e_date = self.switch_caseInitStartDate(e_date, date_str)

      e_expenses = e_date.find(".//expenses") if e_date.find(".//expenses") is not None else self.createChildElement(e_date, 'expenses', None)
      e_expense = et.SubElement(e_expenses, 'expense')

      self.organizeVariation(expense_dict, e_date)

      if 'necessity' in expense_dict:
            e_expense.set('necessity', expense_dict['necessity'])
            del expense_dict['necessity']
      if 'associatedFund' in expense_dict:
            e_expense.set('associatedFund', expense_dict['associatedFund'])
            del expense_dict['associatedFund']
      for key, value in expense_dict.items():
            self.createChildElement(e_expense, key, value)

    def organizeVariation(self, change_dict, e_date):
      e_variation = e_date.find(".//variation") if e_date.find(".//variation") is not None else self.createChildElement(e_date, 'variation', None)
      if 'from' in change_dict:
            if e_variation.find(".//fund".format(change_dict['from'])) is None:
                e_fund = self.createChildElement(e_variation, 'fund', None)
                self.createChildElement(e_fund, 'category', change_dict['from'])
                self.createChildElement(e_fund, 'out', '0.0')
                self.createChildElement(e_fund, 'in', '0.0')
            e_fund_variety = e_variation.find(".//fund/out".format(change_dict['from']))
            e_fund_variety.text = str((Decimal(e_fund_variety.text) + Decimal(change_dict['value'])).quantize(Decimal('0.00')))
            self.modifyBalance(change_dict['from'], Decimal(change_dict['value'])*(-1))

            self.organizeAssociatedFund(e_variation, change_dict, 'from')
      if 'to' in change_dict:
            if e_variation.find(".//fund".format(change_dict['to'])) is None:
                e_fund = self.createChildElement(e_variation, 'fund', None)
                self.createChildElement(e_fund, 'category', change_dict['to'])
                self.createChildElement(e_fund, 'out', '0.0')
                self.createChildElement(e_fund, 'in', '0.0')
            e_fund_variety = e_variation.find(".//fund/in".format(change_dict['to']))
            e_fund_variety.text = str((Decimal(e_fund_variety.text) + Decimal(change_dict['value'])).quantize(Decimal('0.00')))
            self.modifyBalance(change_dict['to'], Decimal(change_dict['value']))

            self.organizeAssociatedFund(e_variation, change_dict, 'to')

    def modifyBalance(self, fund_category, increment_value: Decimal):
      """
      Describe:

      Args:
            fund_category: int or str
                存款账户类型
            increment_value: Decimal
                余额增量,为正或负
      """
      e_value = self.e_dailyAccountBook.find(".//balance").find(".//fund".format(fund_category)).find(".//value")
      e_value.text = str((Decimal(e_value.text) + increment_value).quantize(Decimal('0.00')))

    def organizeAssociatedFund(self, e_variation, change_dict, from_or_to):
      print(change_dict['associatedFund'])
      if 'associatedFund' in change_dict and change_dict['associatedFund'] != 'None':
            print('执行了associatedFund,操作为', from_or_to)
            if e_variation.find(".//fund".format(change_dict['associatedFund'])) is None:
                e_associated_fund = self.createChildElement(e_variation, 'fund', None)
                self.createChildElement(e_associated_fund, 'category', change_dict['associatedFund'])
                self.createChildElement(e_associated_fund, 'out', '0.0')
                self.createChildElement(e_associated_fund, 'in', '0.0')
            if from_or_to == 'from':
                e_fund_variety = e_variation.find(".//fund/out".format(change_dict['associatedFund']))
                flag = -1
            elif from_or_to == 'to':
                e_fund_variety = e_variation.find(".//fund/in".format(change_dict['associatedFund']))
                flag = 1
            else:
                print('未知的收支动作!')
                return
            e_fund_variety.text = str((Decimal(e_fund_variety.text) + Decimal(change_dict['value'])).quantize(Decimal('0.00')))
            self.modifyBalance(change_dict['associatedFund'], Decimal(change_dict['value'])*flag)三、开发总结

1. ElementTree模块基本使用方法

//Element基本构成
texttail
文本尾部
//解析文件
xml_tree = ElementTree.parse("AccountBookFile.xml")
//获取根元素
root = xml_tree.getroot()
//查找子元素:按元素名查找,返回匹配到的第一个元素
//举例:查找root元素的第一个year子元素
//情景含义:找到xml文件的第一个年份的动账记录
year = root.find(".//year")
//查找子元素:按元素名查找,返回所有匹配到的元素
//举例:查找year元素的所有month元素
//情景含义:找到年份中所有月份的动账记录
month_list = year.findall(".//month")
//查找子元素:查找具有特定属性值的子元素
//举例:查找month元素直属子元素中,符合该条件的子元素——具有属性value=10
//情景含义:找到第一个月份中10号日期的动账记录
day = month_list.find(".//day[@value='10']")
//查找子元素:查找具有特定子元素值的子元素
//举例:查找variation元素直属子元素中,符合该条件的子元素——包含文本值为0的category子元素
//情景含义:找到10号微信零钱(category=0)的收支总和
variation = day.find(".//variation")
fund = variation.find(".//fund")
//查找子元素:查找具有特定属性值,同时有特定子元素值的子元素
//举例:查找expenses元素直属子元素中,符合该条件的子元素——具有属性necessity=True,同时包含文本值为0的category子元素
//情景含义:找到10号基本开支中用于饮食(category=0)的花销记录
expenses = day.find(".//expenses")
expense = expenses.find(".//expense[@necessity='True']")
//查找非直属子元素:根据路径查找子元素
//举例:查找root元素下,具有属性value=2023的year元素下,具有属性value=09的month元素下,具有属性value=11的day元素下,所有expenses元素下,包含文本值为1的category子元素的expense元素
//情景含义:找到2023/09/11这一天用于出行(category=1)的花销记录
expense = root.findall(".//year[@value='2023']/month[@value='09']/day[@value='11']/expenses/expense")
//创建子元素
//举例:为e_parent元素创建子元素e_child,子元素名(tag)为child_name,子元素属性(attrib)键值对包含在字典attr_dict中,子元素文本值(text)为child_text
e_child = ElementTree.SubElement(e_parent, child_name, attrib=attr_dict)
e_child.text = child_text
//添加子元素
//举例:将现有的e_child元素设置为e_parent元素的子元素
e_parent.append(e_child)
//修改element元素的名称和文本
element.tag = "new_tag"
element.text = "new_text"
//新增或修改element元素中名为key的属性(attrib本质上是字典)
element.attrib = "new_value"
//删除element元素中名为key的属性,并返回对应的值value。若不存在该属性,则返回defalt_value
value = element.attrib.pop(key, defalt_value)
//写入xml文件
xml_tree.write(file_path, encoding='utf-8', xml_declaration=True)
2. 修改xml文件后,出现原有内容与新增内容格式不一致的情况

参考:https://blog.csdn.net/u012692537/article/details/101395192
通过该函数美化一下,再调用write写入,问题解决
def pretty_xml(element, indent, newline='\n', level=0):# elemnt为传进来的Elment类,参数indent用于缩进,newline用于换行
    if element:# 判断element是否有子元素
      if (element.text is None) or element.text.isspace():# 如果element的text没有内容
            element.text = newline + indent * (level + 1)
      else:
            element.text = newline + indent * (level + 1) + element.text.strip() + newline + indent * (level + 1)
            # else:# 此处两行如果把注释去掉,Element的text也会另起一行
            # element.text = newline + indent * (level + 1) + element.text.strip() + newline + indent * level
    temp = list(element)# 将element转成list
    for sub_element in temp:
      if temp.index(sub_element) < (len(temp) - 1):# 如果不是list的最后一个元素,说明下一个行是同级别元素的起始,缩进应一致
            sub_element.tail = newline + indent * (level + 1)
      else:# 如果是list的最后一个元素, 说明下一行是母元素的结束,缩进应该少一个
            sub_element.tail = newline + indent * level
      pretty_xml(sub_element, indent, newline, level=level + 1)# 对子元素进行递归操作3. QTableWidget单元格内置控件

核心方法:
def setCellWidget(row: int, column: int, widget: QWidget) -> None获QTableWidget中发出信号的控件所在行号的槽函数:
def get_triggeredObj_pos():
    triggeredObj = self.sender() # 获取信号发出者
    # 假设层次关系为triggeredObj放在widget中,而widget放在QTableWidget的单元格中
    widget = triggeredObj.parent()
    table_widget = widget.parent()
    row = table_widget.indexAt(widget.pos()).row()
    print(f"The row number of the button is {row}")
来源:https://www.cnblogs.com/LinfengBingyi/p/17716008.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 【日常收支账本】【Day03】完成编辑账本界面的新增动账记录功能——通过Ele