軟件開(kāi)發(fā)環(huán)境
python 3.7.3
pycharm Community 2020
教師端控制界面

- 下拉列表顯示全部已經(jīng)連接的客戶端ip地址。
- 選中某個(gè)設(shè)備可查看設(shè)備信息,進(jìn)行重啟電腦,關(guān)閉電腦,鎖屏等操作。
- 教師端具備廣播功能,可以自定義教師廣播的文本內(nèi)容。
- 監(jiān)控所有已連接的學(xué)生端屏幕,可以點(diǎn)擊查看大圖。
- 系統(tǒng)信息顯示欄,可以顯示教師端的IP地址和端口號(hào);顯示學(xué)生端設(shè)備信息,可以顯示對(duì)學(xué)生端電腦作出的重啟、關(guān)閉、鎖屏等。
學(xué)生端軟件界面

學(xué)生端軟件包括如下內(nèi)容:
- 連接教師端,需要輸入教師端的網(wǎng)址和端口號(hào)
- 操作面板提示做的各項(xiàng)操作內(nèi)容和老師的廣播內(nèi)容
- 可以向教師端傳輸文件,傳輸文件大小不超過(guò)1Mb
- 舉手簽到操作,點(diǎn)擊舉手后教師端可以即時(shí)看到舉手信息
軟件技術(shù)規(guī)格:
1、軟件包括學(xué)生端和教師端,采用局域網(wǎng)通信。
2、教師端能夠連接至少2臺(tái)學(xué)生端。
3、學(xué)生端能夠向教師端發(fā)送文件。
4、學(xué)生端能夠進(jìn)行簽到舉手操作。
5、教師端能夠向?qū)W生端設(shè)備廣播信息,信息內(nèi)容可自定義。
6、教師端可以對(duì)學(xué)生端屏幕進(jìn)行查看。
7、教師端可以對(duì)學(xué)生端計(jì)算機(jī)進(jìn)行重啟操作。
8、教師端可以對(duì)學(xué)生端計(jì)算機(jī)進(jìn)行關(guān)閉操作。
9、教師端可以對(duì)學(xué)生端計(jì)算機(jī)進(jìn)行鎖屏操作。
10、教師端能夠接收學(xué)生端上傳的文件,狀態(tài)欄顯示自動(dòng)接收文件的信息。
11、教師端能夠查看學(xué)生端計(jì)算機(jī)的狀態(tài)如CPU占用, 狀態(tài)欄顯示狀態(tài)信息。
12、教師端能偶查看學(xué)生端的簽到舉手操作,從狀態(tài)欄顯示。
需求詳細(xì)分析
控制學(xué)生端,發(fā)送信息,發(fā)送文件,發(fā)送監(jiān)控視頻,都要基于數(shù)據(jù)傳輸。因此首先選定教師端和學(xué)生端的通信協(xié)議為T(mén)CP/IP。
1、基于此通信協(xié)議后,教師端需要接收:客戶端的信息,舉手信息,設(shè)備狀態(tài),文件;
2、需要發(fā)送:自定義廣播內(nèi)容,重啟、關(guān)機(jī)、鎖屏等命令。相對(duì)于教師端的接收和操作,每個(gè)學(xué)生端就應(yīng)具備相應(yīng)的發(fā)送和接收功能。
3、教師端對(duì)學(xué)生端屏幕監(jiān)控功能。學(xué)生端需要具備靜默運(yùn)行的屏幕截取功能,并發(fā)送到服務(wù)端。服務(wù)端能夠接收到全部已經(jīng)連接的學(xué)生端截屏,并顯示在屏幕上。
軟件架構(gòu)
應(yīng)用架構(gòu)
根據(jù)以上的需求分析,軟件的架構(gòu)框圖如下圖所示。

軟件在界面的支撐下,需要實(shí)現(xiàn)TCP服務(wù)端和監(jiān)控端的功能。TCP服務(wù)器具備相應(yīng)的接收和發(fā)送功能。教師端的監(jiān)控功能持續(xù)接收學(xué)生端的屏幕截圖。

在軟件架構(gòu)的指導(dǎo)下,我們將軟件分為以上多條線實(shí)現(xiàn),包括了Server開(kāi)啟,接收Client的信息,并能夠發(fā)送信息;接收屏幕截圖。所有邏輯運(yùn)行的線路,用多線程方式運(yùn)行,邏輯線程同界面線程分離,避免界面假死,提高界面的友好度。
數(shù)據(jù)傳輸?shù)脑?/h5>
TCP傳輸?shù)脑砜梢詤⒄站W(wǎng)絡(luò)相關(guān)資料。如下是TCP進(jìn)行傳輸?shù)娜挝帐诌^(guò)程說(shuō)明。
TCP三次握手的過(guò)程如下:
客戶端發(fā)送SYN(SEQ=x)報(bào)文給服務(wù)器端,進(jìn)入SYN_SEND狀態(tài)。
服務(wù)器端收到SYN報(bào)文,回應(yīng)一個(gè)SYN (SEQ=y)ACK(ACK=x+1)報(bào)文,進(jìn)入SYN_RECV狀態(tài)。
客戶端收到服務(wù)器端的SYN報(bào)文,回應(yīng)一個(gè)ACK(ACK=y+1)報(bào)文,進(jìn)入Established狀態(tài)。
屏幕截圖的原理
python進(jìn)行屏幕截圖,采用pyautogui對(duì)屏幕進(jìn)行截取,并使用HTTPServer來(lái)發(fā)送屏幕截圖。
pyautogui是對(duì)屏幕、鼠標(biāo)、鍵盤(pán)進(jìn)行控制的python庫(kù),調(diào)用screenshot()將返回Image對(duì)象,該screenshot()功能大約需要100毫秒。
如果你不需要截取整個(gè)屏幕,還有一個(gè)可選的region參數(shù)。你可以把截取區(qū)域的左上角XY坐標(biāo)值和寬度、高度傳入截取。其文檔位于https://pyautogui.readthedocs.io/en/latest/。
http server就是web server,或者說(shuō)網(wǎng)頁(yè)服務(wù)器,網(wǎng)站服務(wù)器。常用的web server有iis,apache等。iis是internet information server的簡(jiǎn)稱,是windows上主要的web服務(wù)器。
界面開(kāi)發(fā)基礎(chǔ)
PYQT5開(kāi)發(fā)的方法。采用QtDesigner來(lái)簡(jiǎn)單設(shè)計(jì)一個(gè)界面,并保存為一個(gè)文件名為ui的文件。
如下提供了一個(gè)python調(diào)用ui文件并顯示出界面的方法。
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
loadUi("ui_source_server/server_panel.ui", self) # 加載面板文件,使用qt designer開(kāi)發(fā)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.setStyleSheet(qss_style)
window.show()
sys.exit(app.exec_())
如果希望對(duì)軟件界面進(jìn)行美化,那么就在main中加入qss文件。qss是類似于CSS的文件,下文將做出解釋。
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
with open('style_source_server/app_style.qss', encoding='utf-8') as f:
qss_style = f.read()
window.setStyleSheet(qss_style)
window.show()
sys.exit(app.exec_())
實(shí)現(xiàn)路徑
1、界面初步實(shí)現(xiàn)


2、屏幕錄制
采用PyAutoGUI.screenshot()完成屏幕截圖。
HTTPServer提供截圖頁(yè)面。
from http.server import BaseHTTPRequestHandler, HTTPServer
import PyAutoGUI
import socket
PORT = 8008
# 獲取學(xué)生機(jī)局域網(wǎng)地址
IP = socket.gethostbyname(socket.gethostname())
#windows
class myHandler(BaseHTTPRequestHandler):
def do_GET(self):
img = PyAutoGUI.screenshot() #屏幕截圖
if img:
self.send_response(200) #HTTP 狀態(tài)碼
self.send_header('Content-Type', 'image/png')
self.end_headers()
img.save(self.wfile, 'PNG') # 寫(xiě)入HTTP 響應(yīng)流文件
def main():
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#判斷當(dāng)前端口是否已經(jīng)打開(kāi)
result = sock.connect_ex((IP, PORT))
portopen = result == 0
sock.close()
if not portopen:
#啟動(dòng)web服務(wù)器,用自定義的響應(yīng)處理類
server = HTTPServer((IP, PORT), myHandler)
server.serve_forever() # 服務(wù)器持續(xù)監(jiān)聽(tīng)
except:
pass
if __name__ == '__main__':
main()
如果單獨(dú)運(yùn)行屏幕截取的程序后,在瀏覽器中打開(kāi)localhost:8008,就能立刻看到截取的屏幕圖像了。如果手動(dòng)刷新頁(yè)面,截取的屏幕圖像也隨之刷新。
-
連接上后端的數(shù)據(jù)
教師的服務(wù)端程序可以連接上學(xué)生端的屏幕圖像,再設(shè)置輪詢以后就可以顯示截屏了。該種方法實(shí)現(xiàn)的屏幕監(jiān)控具有弊端,在總結(jié)部分有解釋。
界面上顯示后端的
eval('self.video_' + str(i + 1)).setPixmap(video)
在pyqt5中使用setPixmap將圖片顯示到label中。
4.軟件界面和邏輯的分離
python中如果采用PYQT5開(kāi)發(fā)界面,強(qiáng)烈推薦使用多線程來(lái)開(kāi)發(fā)界面和邏輯分離的部分。如下中self.check_thread來(lái)開(kāi)啟多線程程序,程序的回調(diào)信號(hào)為pyqtSignal。回調(diào)信號(hào)的參數(shù)類型可以是python自帶的常規(guī)函數(shù)類型,如int,string,list等,如果是其他更復(fù)雜的類型,可以存放到list類型中進(jìn)行傳遞。
多線程的函數(shù)返回值觸發(fā)回調(diào)函數(shù),如下self.server_callback?;卣{(diào)函數(shù)的參數(shù)value即回調(diào)信號(hào)的參數(shù)。
self.check_thread = Server([]) # 多線程去獲取
self.check_thread.signal.connect(self.server_callback)
self.check_thread.start() # 啟動(dòng)線程
class Server(QThread):
signal = pyqtSignal(list) # 括號(hào)里填寫(xiě)信號(hào)傳遞的參數(shù)
def __init__(self, args_list):
super().__init__()
self.args_data = args_list
def __del__(self):
self.wait()
def run(self):
while True:
client_socket, client_address = socket_server.accept()
if client_address not in conn_list:
conn_list.append(client_address)
conn_dt[client_address] = client_socket
self.signal.emit([client_socket, client_address]) # 發(fā)射信號(hào)
def server_callback(self, value):
"""
Sever 回調(diào)函數(shù)功能
:param value:
:return:
"""
5.子窗口的實(shí)現(xiàn)
點(diǎn)擊每個(gè)學(xué)生端的查看按鈕,可以對(duì)學(xué)生端的屏幕進(jìn)行詳細(xì)查看。
父窗口連接子窗口。
self.child = Child(students[client_num - 1]) # 創(chuàng)建子窗口實(shí)例
self.child.exec()
傳入一個(gè)參數(shù),參數(shù)信息為當(dāng)前查看的學(xué)生信息。
class Child(QDialog):
"""
查看客戶端詳情頁(yè)。
"""
def __init__(self, label, parent=None):
super().__init__(parent)
self.student = label
self.initUI()
def initUI(self):
loadUi("ui_source_server/child_panel.ui", self)
self.setWindowTitle("多媒體管理軟件 -櫻桃智庫(kù)") # 設(shè)置窗口標(biāo)題
self.pushButton_quit.clicked.connect(self.quit) # 點(diǎn)擊ok,隱士存在該方法
self.timer = QTimer(self) # 初始化一個(gè)定時(shí)器
if self.student:
self.timer.timeout.connect(self.get_client_screen) # 每次計(jì)時(shí)到時(shí)間時(shí)發(fā)出信號(hào)
self.timer.start(600) # 設(shè)置計(jì)時(shí)間隔并啟動(dòng);單位毫秒
else:
pass
def get_client_screen(self):
self._thread = AutoPollScreen([self.student]) # 多線程去獲取
self._thread.signal.connect(self.screen_callback)
self._thread.start() # 啟動(dòng)線程
def screen_callback(self, videos):
video = videos[0].scaled(1200, 800)
self.label.setPixmap(video)
def quit(self): # 點(diǎn)擊ok是發(fā)送內(nèi)置信號(hào)
self.timer.stop()
self.close()
開(kāi)發(fā)完畢

其實(shí)可以更進(jìn)一步地對(duì)界面的美化進(jìn)行提高,但想想也沒(méi)必要了!

異常捕獲
1、屏幕截圖慢。
教師控制端采用輪詢的方式連接學(xué)生端的http服務(wù)器,會(huì)導(dǎo)致屏幕監(jiān)控出現(xiàn)卡頓或者幀數(shù)較慢的情況出現(xiàn)。針對(duì)此問(wèn)題的解決辦法可以借鑒騰訊會(huì)議的屏幕分享功能。
2、學(xué)生端關(guān)閉程序,教師端會(huì)卡死。
這個(gè)問(wèn)題主要是因?yàn)門(mén)CP的連接造成的,TCP連接成功后應(yīng)該主動(dòng)斷開(kāi)連接,否則會(huì)拋出異常。但是存在的難點(diǎn)是,如果學(xué)生端非正常退出,也沒(méi)有辦法將退出信號(hào)提前發(fā)送給教師端。
參考資料
暫無(wú)。
項(xiàng)目地址
https://github.com/Jarrettluo/multimedia_management_software
以上項(xiàng)目作為聯(lián)系項(xiàng)目,距離真正商用還有很遠(yuǎn)的距離。僅提供畢業(yè)設(shè)計(jì)或軟件開(kāi)發(fā)的參考項(xiàng)目。如果對(duì)其他的實(shí)現(xiàn)方法感興趣,我們可以從開(kāi)源地址聯(lián)系我,我們共同探討!
歡迎點(diǎn)贊、轉(zhuǎn)發(fā)、分享!
