在經過前面幾篇文章對 WebRTC 的描述,相信已經不需再過多對它介紹了。前面幾篇文章我們實現了 Web 、Android 端的音視頻通話項目,該篇我們使用 QT UI 框架搭建 Windows 端的多 P2P 音視頻通話實戰項目。
項目地址:github.com/yangkun1992…
最終與 Android、Web 端運行后的效果如下:
首選我們去下載 QT 6.6.0 最新版本,下載地址為: www.qt.io/download ,安裝好后選擇 cmake 進行構建項目
2.1 clone 信令服務器代碼
git clone https://github.com/yangkun19921001/OpenRTCProject.git
2.2 啟動信令服務器
cd p2ps/server
//修改你自己的證書
const server=process.env.HTTPS==='true' ? http.createServer({
key : fs.readFileSync('./cert/rtcmedia.top.key'),
cert: fs.readFileSync('./cert/rtcmedia.top_bundle.pem')
}, app): http.createServer(app);
//執行啟動命令
./server_run.js
./proxy_server_run.js
//查看是否啟動成功
lsof -i:8880 和 443
git clone https://github.com/yangkun19921001/OpenRTCClient.git
按照 README 進行編譯,編譯完成后拿到靜態庫 build/win/debug/obj/webrtc.lib
4. socketio-client-cpp 2.0.0 靜態庫準備
按照官方文檔進行編譯,可參考:github.com/socketio/so…
5. QT cmake 編寫
上序 4 步如果都準備好,那么就可以通過 cmake 進行將其依賴進來,如下所示:
cmake_minimum_required(VERSION 3.5)
project(p2ps VERSION 0.1 LANGUAGES CXX)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(BUILD_TYPE debug)
if(MSVC)
if(CMAKE_BUILD_TYPE MATCHES Debug)
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd")
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /MTd")
elseif(CMAKE_BUILD_TYPE MATCHES Release)
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT")
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /MT")
#set(BUILD_TYPE release)
endif()
endif()
set(WEBRTC_THIRD_PARTY_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../../webrtc/third_party)
set(LIBWEBRTC_INCLUDE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../../webrtc)
set(LIBWEBRTC_BINARY_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../../build_system/build/win/x64/${BUILD_TYPE}/obj)
set(DEPS_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/deps)
set(SOCKET_IO_BINARY_PATH ${DEPS_ROOT_PATH}/socketio/win/x64/${BUILD_TYPE})
set(SOCKET_IO_INCLUDE_PATH ${DEPS_ROOT_PATH}/socketio/include)
set(JSONCPP_SOURCE
"${WEBRTC_THIRD_PARTY_PATH}/jsoncpp/source/src/lib_json/json_reader.cpp"
"${WEBRTC_THIRD_PARTY_PATH}/jsoncpp/source/src/lib_json/json_tool.h"
"${WEBRTC_THIRD_PARTY_PATH}/jsoncpp/source/src/lib_json/json_value.cpp"
"${WEBRTC_THIRD_PARTY_PATH}/jsoncpp/source/src/lib_json/json_writer.cpp"
)
target_include_directories(${PROJECT_NAME} PUBLIC
"${LIBWEBRTC_INCLUDE_PATH}"
"${LIBWEBRTC_INCLUDE_PATH}/third_party/abseil-cpp"
"${LIBWEBRTC_INCLUDE_PATH}/third_party/jsoncpp/source/include"
"${LIBWEBRTC_INCLUDE_PATH}/third_party/jsoncpp/generated"
"${LIBWEBRTC_INCLUDE_PATH}/third_party/libyuv/include"
"${SOCKET_IO_INCLUDE_PATH}"
)
add_definitions(
#webrtc & qt 沖突
-DQT_DEPRECATED_WARNINGS
-DQT_NO_KEYWORDS
#jsoncpp
-DJSON_USE_EXCEPTION=0
-DJSON_USE_NULLREF=0
#socketio
#-DSIO_TLS
-DUSE_AURA=1
-D_HAS_EXCEPTIONS=0
-D__STD_C
-D_CRT_RAND_S
-D_CRT_SECURE_NO_DEPRECATE
-D_SCL_SECURE_NO_DEPRECATE
-D_ATL_NO_OPENGL
-D_WINDOWS
-DCERT_CHAIN_PARA_HAS_EXTRA_FIELDS
-DPSAPI_VERSION=2
-DWIN32
-D_SECURE_ATL
-DWINUWP
-D__WRL_NO_DEFAULT_LIB__
# -DWINAPI_FAMILY=WINAPI_FAMILY_PC_APP
-DWIN10=_WIN32_WINNT_WIN10
-DWIN32_LEAN_AND_MEAN
-DNOMINMAX
-D_UNICODE
-DUNICODE
-DNTDDI_VERSION=NTDDI_WIN10_RS2
-D_WIN32_WINNT=0x0A00
-DWINVER=0x0A00
-DDEBUG
-DNVALGRIND
-DDYNAMIC_ANNOTATIONS_ENABLED=0
-DWEBRTC_ENABLE_PROTOBUF=0
-DWEBRTC_INCLUDE_INTERNAL_AUDIO_DEVICE
-DRTC_ENABLE_VP9
-DHAVE_SCTP
-DWEBRTC_LIBRARY_IMPL
-DWEBRTC_NON_STATIC_TRACE_EVENT_HANDLERS=0
-DWEBRTC_WIN
-DABSL_ALLOCATOR_NOTHROW=1
-DHAVE_SCTP
-DWEBRTC_VIDEO_CAPTURE_WINRT)
target_link_libraries(p2ps PRIVATE
...
${SOCKET_IO_BINARY_PATH}/sioclient.lib ${SOCKET_IO_BINARY_PATH}/sioclient_tls.lib
${LIBWEBRTC_BINARY_PATH}/webrtc.lib
winmm.lib iphlpapi.lib wbemuuid.lib secur32.lib advapi32.lib Mmdevapi.lib
Mfuuid.lib msdmo.lib dmoguids.lib wmcodecdspuuid.lib comdlg32.lib dbghelp.lib
dnsapi.lib gdi32.lib msimg32.lib odbc32.lib odbccp32.lib oleaut32.lib shell32.lib
shlwapi.lib user32.lib usp10.lib uuid.lib version.lib wininet.lib winmm.lib
winspool.lib ws2_32.lib delayimp.lib kernel32.lib ole32.lib crypt32.lib
amstrmid.lib strmiids.lib
到此,在 QT 項目中的 WebRTC 和 SocketIO 環境已經搭建完成了,這里為什么要搭建 SocketIO 呢?因為在之前的 web 、server、Android 都使用的是 socketio 開源庫來做的信令通信,正好它又是跨平臺的,所以就不再更換了。
如何構建多人音視頻通話?
要在 QT 中構建一個多人的音視頻通話項目,必定要經過如下幾個步驟
相關學習資料推薦,點擊下方鏈接免費報名,先碼住不迷路~】
音視頻免費學習地址:https://xxetb.xet.tech/s/2cGd0
【免費分享】音視頻學習資料包、大廠面試題、技術視頻和學習路線圖,資料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以點擊788280672加群免費領取~
server 端的信令其實設計的很簡單,就三組信令,如下所示:
join > joined :加入房間和加入成功的通知 leave > leaved:離開房間和離開成功的通知 message:交換 offer ,candidate
更加詳細的流程可以參考下面的流程圖
首先定義 ISinnalClient.h 抽象接口
namespace PCS{
class ISignalClient {
public:
enum class SignalEvent {
JOINED=0,
JOIN,
LEAVED,
LEAVE,
MESSAGE
};
static std::string SignalEventToString(SignalEvent event) {
switch(event) {
case SignalEvent::JOINED: return "joined";
case SignalEvent::LEAVED: return "leaved";
case SignalEvent::JOIN: return "join";
case SignalEvent::LEAVE: return "leave";
case SignalEvent::MESSAGE: return "message";
default: return "unknown";
}
}
/*連接服務端*/
virtual bool connect(const std::string url,OnSignalEventListener* listener)=0;
/*加入會話*/
virtual void join(const std::string roomId)=0;
/*離開會話*/
virtual void leave(const std::string roomId)=0;
/*銷毀 client */
virtual void release()=0;
/*發送消息*/
virtual void sendMessage(const std::string roomId, const std::string remoteId, const std::string message)=0;
virtual std::string getSocketId()=0;
virtual ~ISignalClient()=default;
};
}
然后 socketio 根據對應的 api 去封裝實現即可,詳細的使用,可以參考 github.com/yangkun1992…
多 PeerConnection 管理其實就是把每次加入房間的 Peer 添加到一個容器中,管理起來。這里我們可以定義一個 map 結構進行管理,核心 api 如下
3.1 定義一個 PeerConnection 管理結構體
struct Peer
{
Peer() {}
rtc::scoped_refptr<webrtc::PeerConnectionInterface> peer_conn_inter_;
std::unique_ptr<PeerConnectionObserverImpl> peer_conn_obser_impl_;
std::unique_ptr<CreateSessionDescriptionObserImpl> create_offer_sess_des_impl_;
std::unique_ptr<CreateSessionDescriptionObserImpl> create_answer_sess_des_impl_;
};
內部主要包含每個 PeerConnection 所需要的成員,然后通過 std::map 進行管理,如下所示
std::map<std::string ,std::unique_ptr<Peer>> peers_;
map 中的key 就是連接到房間中的 id
3.2 創建 PeerConnectionFactory
通過如下核心代碼即可創建出 PeerConnectionFactory
peer_connection_factory_=webrtc::CreatePeerConnectionFactory(
this->network_thread_.get() /* network_thread */,
this->worker_thread_.get() /* worker_thread */,
this->signaling_thread_.get(), /* signaling_thread */
nullptr /* default_adm */,
webrtc::CreateBuiltinAudioEncoderFactory(),
webrtc::CreateBuiltinAudioDecoderFactory(),
webrtc::CreateBuiltinVideoEncoderFactory(),
webrtc::CreateBuiltinVideoDecoderFactory(), nullptr /* audio_mixer */,
nullptr /* audio_processing */);
當創建成功后,再創建本地音視頻軌道,后續會將本地音視頻軌道添加到 PeerConnection 中
//音頻軌道
audio_track_=peer_connection_factory_->CreateAudioTrack(
kAudioLabel, peer_connection_factory_->CreateAudioSource(
cricket::AudioOptions()));
//視頻軌道
rtc::scoped_refptr<CameraCapturerTrackSource> video_device=CameraCapturerTrackSource::Create(1280,720,30);
video_track_=peer_connection_factory_->CreateVideoTrack(kVideoLabel, video_device);
3.3 創建 PeerConnection
bool PeerManager::createPeerConnection(const std::string &peerId, const webrtc::PeerConnectionInterface::RTCConfiguration &config
, OnPeerManagerEvents* ets)
{
RTC_LOG(LS_INFO) <<__FUNCTION__ <<" peerId:"<<peerId;
RTC_DCHECK(peer_connection_factory_);
std::unique_ptr<PeerConnectionObserverImpl> peer_conn_obimpl= std::make_unique<PeerConnectionObserverImpl>(peerId,ets);
auto peerConnection=peer_connection_factory_->CreatePeerConnection(
config,
nullptr,
nullptr,
peer_conn_obimpl.get());
if (peerConnection) {
auto peer_ptr=std::make_unique<Peer>();
peer_ptr->peer_conn_inter_=peerConnection;
peer_ptr->peer_conn_obser_impl_=std::move(peer_conn_obimpl);
peers_[peerId]=std::move(peer_ptr);
if(video_track_)
peerConnection->AddTrack(video_track_, { kVideoLabel });
if(audio_track_)
peerConnection->AddTrack(audio_track_, { kAudioLabel });
return true;
}
return false;
}
由上面的代碼得知,我們通過 peer_connection_factory_->CreatePeerConnection 就可以構建一個 PeerConnection ,并把之前創建出來的本地音視頻軌道添加到 PeerConnection 中,最后我們將構建出來的 PeConn 緩存到 map 中,便于后續的處理。
3.4 創建 offer
void PeerManager::createOffer(const std::string &peerId, OnPeerManagerEvents* ets)
{
RTC_LOG(LS_INFO) <<__FUNCTION__ <<" peerId:"<<peerId;
auto it=peers_.find(peerId);
if (it !=peers_.end() && it->second->peer_conn_inter_ !=nullptr) {
auto obs=std::make_unique<CreateSessionDescriptionObserImpl>(true,peerId,ets);
it->second->peer_conn_inter_->CreateOffer(obs.get(), webrtc::PeerConnectionInterface::RTCOfferAnswerOptions());
it->second->create_offer_sess_des_impl_=std::move(obs);
}else {
RTC_LOG(LS_ERROR) << __FUNCTION__ << " peers_ not found id:"<<peerId;
}
}
上面的代碼,首先根據 peerId 從緩存中拿到對應的 PeerConnection ,然后再調用它內部的 api 來進行 CreateOffer
3.5 設置本地/遠端 SDP
void PeerManager::setLocalDescription(const std::string &peerId,webrtc::SessionDescriptionInterface *desc_ptr)
{
RTC_LOG(LS_INFO) <<__FUNCTION__ <<" peerId:"<<peerId;
auto pt=peers_.find(peerId);
if (pt !=peers_.end()) {
pt->second->peer_conn_inter_->SetLocalDescription(SetSessionDescriptionObserverImpl::Create(),desc_ptr);
}else {
RTC_LOG(LS_ERROR) << __FUNCTION__ << " peers_ not found id:"<<peerId;
}
}
void PeerManager::setRemoteDescription(const std::string &peerId,webrtc::SessionDescriptionInterface *desc_ptr)
{
RTC_LOG(LS_INFO) <<__FUNCTION__ <<" peerId:"<<peerId;
auto pt=peers_.find(peerId);
if (pt !=peers_.end()) {
pt->second->peer_conn_inter_->SetRemoteDescription(SetSessionDescriptionObserverImpl::Create(),desc_ptr);
}else {
RTC_LOG(LS_ERROR) << __FUNCTION__ << " peers_ not found id:"<<peerId;
}
}
從緩存中拿到對應的 PeerConnection ,然后設置本地或遠端的 sdp 描述信息
3.6 創建 answer
void PeerManager::createAnswer(const std::string &peerId, OnPeerManagerEvents* ets)
{
RTC_LOG(LS_INFO) <<__FUNCTION__ <<" peerId:"<<peerId;
auto it=peers_.find(peerId);
if (it !=peers_.end() && it->second->peer_conn_inter_ !=nullptr) {
auto obs=std::make_unique<CreateSessionDescriptionObserImpl>(false,peerId,ets);
it->second->peer_conn_inter_->CreateAnswer(obs.get(), webrtc::PeerConnectionInterface::RTCOfferAnswerOptions());
it->second->create_answer_sess_des_impl_=std::move(obs);
}else {
RTC_LOG(LS_ERROR) << __FUNCTION__ << " peers_ not found id:"<<peerId;
}
}
此處與上一步邏輯一樣
3.7 處理 ice
void PeerManager::handleCandidate(std::string peerId, std::unique_ptr<webrtc::IceCandidateInterface> candidate)
{
RTC_LOG(LS_INFO) << __FUNCTION__ <<" peerId:"<<peerId;
auto pt=peers_.find(peerId);
if (pt !=peers_.end()) {
pt->second->peer_conn_inter_->AddIceCandidate(
std::move(candidate),
[peerId](webrtc::RTCError error){
if (error.ok()) {
RTC_LOG(LS_INFO) <<" peerId:"<< peerId << " AddIceCandidate success.";
} else {
RTC_LOG(LS_INFO) <<" peerId:"<< peerId << "AddIceCandidate failed, error: " << error.message();
}
});
}else {
RTC_LOG(LS_ERROR) << __FUNCTION__ << " peers_ not found id:"<<peerId;
}
}
也是從緩存中拿到對應的 PeerConnection ,然后將對方的候選者地址添加進去。
定義 RTCRoomManager ,實現信令回調和PeerManager 回調,定義的核心 API 如下
namespace PCS{
class RTCRoomManager : public OnSignalEventListener,public OnPeerManagerEvents {
public:
RTCRoomManager();
virtual ~RTCRoomManager();
//連接服務器
void connect(const std::string url,OnRoomStateChangeCallback* callback);
//設置本地軌道的回調監聽
void setLocalTrackCallback(std::function<void(std::string,int,int,rtc::scoped_refptr<webrtc::VideoTrackInterface>)> localVideoTrack);
//加入房間
void join(const std::string roomId);
//離開房間
void leave(const std::string roomId);
//銷毀
void release();
//處理 ui 傳遞過來的 消息
void onUIMessage(Message msg);
private:
//實現連接成功的處理代碼
void onConnectSuccessful() override;
//實現正在連接的處理代碼
void onConnecting() override ;
//實現連接錯誤的處理代碼
void onConnectError(const std::string& error) override ;
//實現已加入房間的處理代碼
void onJoined(const std::string& room, const std::string& id, const std::vector<std::string>& otherClientIds) override;
//實現離開房間的處理代碼
void onLeaved(const std::string& room, const std::string& id) override ;
//實現接收到消息的處理代碼
void onMessage(const std::string& from, const std::string& to, const std::string& message) override ;
//當需要添加遠端的軌道
void OnAddTrack(std::string peerid,
rtc::scoped_refptr<webrtc::RtpReceiverInterface> receiver,
const std::vector<rtc::scoped_refptr<webrtc::MediaStreamInterface>>&
streams)override;
//當刪除遠端的軌道
void OnRemoveTrack(std::string peerid,
rtc::scoped_refptr<webrtc::RtpReceiverInterface> receiver) override;
// datachannel 消息
void OnDataChannel(std::string peerid,
rtc::scoped_refptr<webrtc::DataChannelInterface> channel) override;
//ice 消息
void OnIceCandidate(std::string peerid,const webrtc::IceCandidateInterface* candidate) override;
//offer or answer create 成功
void OnCreateSuccess(bool offer,std::string peerid,webrtc::SessionDescriptionInterface* desc) override;
//offer or answer create 失敗
void OnCreateFailure(bool offer,std::string peerid,webrtc::RTCError error) override;
private:
std::unique_ptr<ISignalClient> socket_signal_client_imp_;
std::unique_ptr<PeerManager> peer_manager_;
OnRoomStateChangeCallback * room_state_change_callback_;
std::function<void(std::string,int,int,rtc::scoped_refptr<webrtc::VideoTrackInterface>)> local_track_callback_;
std::string room_id_;
};
} // end namespace PCS
這就是核心 API, 它持有peer_manager_、socket_signal_client_imp_,分別是對 PeerConnection 和信令的交互。
比如現在 A 用戶先進入房間,B 后進入房間,然后對它們的管理流程是這樣的
5. 如何顯示
在 webrtc 架構中,是通過 rtc::VideoSinkInterface<webrtc::VideoFrame> 的 onFrame 虛函數進行通知需要新視頻數據的渲染。
我們定義一個 VideoRendererWidget 然后實現 rtc::VideoSinkInterface<webrtc::VideoFrame> 的 onFrame 函數,如下所示:
namespace PCS {
class VideoRendererWidget : public QOpenGLWidget, protected QOpenGLFunctions,
public rtc::VideoSinkInterface<webrtc::VideoFrame>
{
Q_OBJECT
public:
VideoRendererWidget(std::string peerId="",QWidget* parent=nullptr,webrtc::VideoTrackInterface *track=nullptr);
~VideoRendererWidget();
public Q_SLOTS:
void PlayOneFrame();
protected:
void initializeGL() Q_DECL_OVERRIDE;
void resizeGL(int w, int h) Q_DECL_OVERRIDE;
void paintGL() Q_DECL_OVERRIDE;
public:
// VideoSinkInterface implementation
void OnFrame(const webrtc::VideoFrame& frame) override;
private:
...
public:
webrtc::VideoTrackInterface* video_track_;
};
}
void VideoRendererWidget::OnFrame(const webrtc::VideoFrame &video_frame)
{
std::lock_guard<std::mutex> guard(renderer_mutex_);
rtc::scoped_refptr<webrtc::I420BufferInterface> buffer(
video_frame.video_frame_buffer()->ToI420());
if (video_frame.rotation() !=webrtc::kVideoRotation_0) {
buffer=webrtc::I420Buffer::Rotate(*buffer, video_frame.rotation());
}
int width=buffer->width();
int height=buffer->height();
int ySize=width * height;
int uvSize=ySize / 4; // For each U and V component
if(m_nVideoW==0 && m_nVideoH==0)
{
if(!peer_id_.empty()) RTC_LOG(LS_INFO) << __FUNCTION__ << " init w:"<<width<<" height:"<<height << " peerId:" <<peer_id_;
}
if(width !=m_nVideoW && height !=m_nVideoH && video_data_ !=nullptr)
{
video_data_ .reset();
if(!peer_id_.empty()) RTC_LOG(LS_INFO) << __FUNCTION__ << " change w:"<<width<<" height:"<<height << " peerId:" <<peer_id_;
}
if(video_data_==nullptr){
video_data_=std::make_unique<uint8_t[]>(width * height * 1.5); // Use make_unique to allocate array
if(!peer_id_.empty())
RTC_LOG(LS_INFO) << __FUNCTION__ << " malloc Id:"<<peer_id_<<" width:" << width <<" height:"<<height;
}
memcpy(video_data_.get(), buffer->DataY(), ySize);
memcpy(video_data_.get() + ySize, buffer->DataU(), uvSize);
memcpy(video_data_.get() + ySize + uvSize, buffer->DataV(), uvSize);
m_nVideoW=width;
m_nVideoH=height;
// 刷新界面,觸發paintGL接口
Q_EMIT PlayOneFrame();
}
當我們調用 PlayOneFrame 時,就可以通過 QOpenGL 來進行渲染 I420 YUV 數據了。
6. 如何管理多個窗口的創建和銷毀的
可以通過管理 PeerConnection 那樣管理 VideoRendererWidget ,還是定義一個 map
std::map<std::string, std::unique_ptr<PCS::VideoRendererWidget>> video_renderer_widgets_;
根據對方的 peerid 來進行緩存窗口,
當需要添加窗口時:
void MainWindow::addVideoRendererWidgetToMainWindow(std::string id, std::unique_ptr<PCS::VideoRendererWidget> renderer)
{
const int itemsPerRow=3;
std::unique_ptr<PCS::VideoRendererWidget> videoRenderer;
if(renderer==nullptr)
videoRenderer=std::make_unique<PCS::VideoRendererWidget>();
else {
videoRenderer=std::move(renderer);
}
videoRenderer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
// 計算新的位置
int count=ui->gridLayout->count();
int row=count / itemsPerRow;
int column=count % itemsPerRow;
// 添加到 gridLayout 中
ui->gridLayout->addWidget(videoRenderer.get(),row,column);
//videoRenderer->setFixedSize(1280/3,720/3);
// 將 widget 添加到 map 中
video_renderer_widgets_.insert({id, std::move(videoRenderer)});
}
當需要刪除窗口時:
void MainWindow::removeVideoRendererWidgetFromMainWindow(std::string id)
{
// 從 layout 中移除 widget,并刪除它
// 查找 widget
auto it=video_renderer_widgets_.find(id);
if (it !=video_renderer_widgets_.end())
{
// 從 layout 中移除 widget
ui->gridLayout->removeWidget(it->second.get());
// 從 map 中移除并刪除 widget
video_renderer_widgets_.erase(it);
}
}
我們通過簡短的描述和一些基礎的 api 來介紹了如何通過 QT webrtc 來構建一個多人的音視頻通話的項目。由于本人對 QT 不是太熟悉,所以 UI 上還有少許 Bug 。但不影響核心 API 調用。
到此,通過本篇文章和之前的幾篇文章我們已經實現了 Web 、Android 、Windows 之前的互通,后續會繼續介紹 WebRTC 源碼分析和實戰項目的開發。
原文 WebRTC 實戰: QT for Windows 多人音視頻通話
從這一篇我們正式開始Qt編程。本篇主要講解Qt編程環境的搭建。為了適應大多數讀者的需要,同時為了避免系統環境的不同而產生不必要的問題,這里選擇使用Windows系統的Qt版本。因為在前面幾十篇中我們主要講解基本Qt控件項目的桌面編程,所以沒有使用SDK進行安裝,而是采用了Qt庫與Qt Creator分別下載安裝的方式,這樣就只需要下載Qt的桌面版本的庫。而SDK中默認集成了Qt Creator和Qt桌面庫以及Qt移動開發的庫,這個會在第40篇至第50篇進行講解。再者,鑒于Qt一次編寫代碼,多次編譯運行的特點,在我們教程中講解的例子都是可以直接在其他系統環境下(比如Linux系統)直接編譯運行的。
環境:Windows 7 + Qt 4.8.1+ Qt Creator 2.4.1
一、Qt 及 Qt Creator的下載和安裝
1.下載
(已過時)
下載Qt 4.8.1:ftp://ftp.qt-project.org/qt/source/qt-win-opensource-4.8.1-mingw.exe
下載Qt Creator2.4.1:ftp://ftp.qt-project.org/qtcreator/qt-creator-win-opensource-2.4.1.exe
最新下載地址:(已過時)
所有版本的Qt下載地址:ftp://ftp.qt-project.org/qt/source/
所有版本的Qt Creator下載地址:ftp://ftp.qt-project.org/qtcreator/
最新下載地址:http://download.qt-project.org/
其中snapshots里面包含了最新測試版本;official releases里面包含了官方發布版,即最終發布版; archive里面是Qt4.7及以前版本,Qt Creator2.5及以前版本。
更新 (2013-5-1 已過時)
提示:在最近的Qt Creator版本(2.5.0及以后)中已經默認不再包含MinGW,需要自己手動下載安裝。可以在這里下載。(注:最新的Qt 5版本中已經默認包含了Qt Creator和MinGW,需根據自己實際情況操作。
官方原文如下:
Notefor Windows MinGW Users
Wedecided to remove the custom MinGW distribution and MinGW gdb from our QtCreator-only Windows binary distribution package. The original reas toinclude it there (it was the predecessor of the Qt SDK) are since a while nowfilled by the Qt SDK. Also, updating the shipped version is a legal hassle aslong as the binaries are provided through Nokia, but we also don’t want to shipstone age versi. We are working on build infrastructure for the Qt Projectitself though, that we ultimately want to use to build Qt Creator packages,snapshots, and more. Currently, on http://builds.qt-project.org , you find QtCreator snapshots for Linux and Windows, and also a Python enabled MinGW gdb(that reportedly doesn’t work on Windows XP). It’s still possible to installMinGW and gdb separately and register them in Qt Creator. Weare not removing the support for it from Qt Creator.
Previouslyshipped MinGW: ftp://ftp.qt.nokia.com/misc/MinGW-gcc440_1.zip
Previously shipped MinGW gdb: ftp://ftp.qt.nokia.com/misc/gdb/7.2/qtcreator-gdb-7.2-mingw-x86.zip
Upto date MinGW: http://www.mingw.org (we might provide acompact version like the one in the old installer later)
Python enabled MinGW gdb 7.4: http://builds.qt-project.org/job/gdb-windows/ (compiledon Windows 7, doesn’t work on Windows XP)
更新:2014-10-1
在安裝Qt 4.8.6及以后的Qt 4版本時,應該按照安裝時的提示來下載相應版本的MinGW,不然編譯程序無法運行。例如Qt 4.8.6安裝時的提示如下圖。
可以直接點擊提示給的鏈接來下載。也可以從這里下載。
更新:(關于Qt 4.8搭配Qt Creator 2.5以后版本的MinGW和無法調試的情況,2013-7-1)
注意:Qt 5以后版本默認包含了所有需要的工具,不存在這里的情況,直接下載安裝即可使用!
1.MinGW
如果是Qt 4版本,需要使用GCC 4.4,也就是MinGW需要是4.4版本的,其他新的版本均不可用。
下載:http://pan.baidu.com/share/link?shareid=1521902020&uk=2352291552
備用地址:http://builds.qt-project.org/job/mingw32-windows/lastSuccessfulBuild/artifact/mingw32-qtproject.7z
2.調試器GDB
在Qt 4.8版本,需要下載并指定GDB才能正常調試。
下載地址:http://origin.releases.qt-project.org/gdb/ 或到 社區下載頁面進行下載
從這里面根據自己的系統來下載合適的版本。
下載完MinGW和GDB以后,將其解壓到Qt的安裝目錄中,比如這里都解壓到了C:\Qt目錄中。
3.在Qt Creator中的設置。我們需要先在編譯器中添加并制定gcc的路徑,例如C:\Qt\mingw32\bin\gcc.exe 如下圖所示:
然后在Qt版本中添加并指定qmake的路徑,如下圖所示。
最后在構建套件中添加并指定編譯器、調試器和Qt版本。如下圖所示。
2.安裝
下載完成后先安裝QtCreator,采用默認選項即可,安裝路徑推薦使用默認的C盤,因為這樣可以與教程中的一致,在以后的內容中可以避免一些不必要的問題。然后安裝Qt庫,當在選擇mingw目錄時,需要設置為前面安裝的Qt Creator目錄下的mingw目錄。如下圖所示。
二、創建hello world程序
1.運行Qt Creator 從桌面上的快捷方式打開Qt Creator,進入眼簾的是Qt Creator的歡迎界面。如下圖所示。
Qt Creator分為了七個模式:歡迎模式、編輯模式、設計模式、調試模式、項目模式、分析模式和幫助模式,分別由左側的七個圖標進行切換,對應的快捷鍵是Ctrl + 數字1到7。現在顯示的就是歡迎界面,這里可以看到一些入門教程、開發的項目列表、Qt提供的示例程序,也可以創建或打開一個項目。
2.創建項目
我們使用歡迎頁面上方的“創建項目”按鈕來創建新的項目(當然也可以在文件菜單中創建項目)。在項目模板中選擇Qt 控件項目,然后選擇QtGui應用,這樣便會生成一個一般的桌面Qt圖形界面項目,如下圖所示。其他項目的創建會在后面的教程中講到。
然后更改項目名稱和路徑,這里名稱可以設置為helloworld,注意名稱和路徑上都不要有中文。如下圖所示。
點擊下一步后,會彈出目標設置對話框,這里顯示沒有有效的Qt版本,并提示需要在工具/選項菜單中進行設置。如下圖所示。下面我們就來添加Qt版本。
3.關聯Qt庫
因為我們這里是分別下載Qt Creator和Qt庫的方式,所以安裝后它們并沒有關聯,這樣是無法編譯程序的。下面在Qt Creator中關聯Qt庫。打開工具→選項菜單,然后選擇“構建和運行”一項,再進入Qt版本選項卡。如下圖所示。
我們可以手動設置Qt版本的關聯,現在點擊右上角的“添加”按鈕,然后會讓選擇qmake.exe文件,我們在Qt(不是Qt Creator)安裝目錄的bin目錄中找到該文件并打開。如下圖所示。
現在已經默認生成了版本信息,我們點擊確定按鈕即可。如下圖所示。
當設置完Qt版本,再次回到歡迎界面后,可以發現“演示和范例”中已經顯示出了各種示例程序,大家可以打開自己需要的一個例子。這個我們先不進行講解,下面繼續來完成hello world程序。
4.完成hello world項目
還按照前面的流程創建項目,在目標設置頁面默認選擇為了桌面Qt版本,因為現在我們只關聯了這一個桌面版本的Qt庫,所以只能編譯為桌面程序。如下圖所示。這里可以選中“使用影子構建”,這樣編譯生成的文件會和源碼分別存放,這個在下面的內容中會看到。
點擊下一步,在顯示的類信息中將基類選擇為QDialog,就是說我們將程序設置為了一個對話框,然后將類名更改為MyDialog。如下圖所示。
再點擊下一步進入匯總頁面,這里可以選擇版本控制系統,我們這里沒有用到,所以不進行設置,點擊完成按鈕來完成項目的創建。如下圖所示。
5.編輯運行項目
創建完項目后會進入編輯模式,這里可以對項目文件進行查看和編輯。左側是項目文件的列表,這里將項目中的文件分為了頭文件、源文件等,進行分類顯示。除了顯示項目文件,還可以通過下拉菜單來選擇類視圖、大綱等內容。在右側就是代碼編輯區域,這里對關鍵字進行了高亮顯示。如下圖所示。
我們雙擊界面文件中的mydialog.ui文件,進入設計模式。在這里可以對界面進行可視化設計,也就是所見即所得。左側的是一些常用部件,可以直接拖動到界面上;右側是對象和類列表,下面是部件的屬性編輯窗口;在中間,上方是主設計區域,顯示了窗口的主界面,下面是Action編輯器以及信號和槽編輯器窗口。
我們從左側部件列表中找到Label標簽部件并拖動到界面上,然后雙擊,更改其顯示文本為“helloworld”,如下圖所示。
下面我們單擊Qt Creator左側的
運行按鈕來編譯運行程序,這時會彈出保存修改對話框,如下圖所示。這里選中“構建之前總是先保存文件”,然后點擊保存所有文件按鈕。
最后hello world程序成功運行,效果如下圖所示。
三、發布程序
1.查看工程目錄
這里會發現多了一個helloworld-build-desktop-Qt_4_8_1__4_8_1____目錄,里面存放的就是編譯生成的文件。這就是前面創建項目講到的 “使用影子構建” ,如果沒有選中這個,那么生成的文件就會和源碼在同一個目錄里。該目錄的內容如下圖所示。
這里有兩個目錄:debug和release,分別用于存放debug方式和release方式編譯生成的可執行文件。因為編譯時默認是debug版本,所以現在release目錄中是空的。打開debug目錄,可以看到生成的可執行文件helloworld.exe如下圖所示。
此時雙擊helloworld.exe文件,會彈出系統錯誤提示框,表明丟失了mingwm10.dll文件。如下圖所示。
其實我們可以在Qt安裝路徑下找到該文件,我這里是在C:\Qt.8.1\bin中,將其復制到debug目錄里面,然后還會提示缺少其他幾個dll文件,依次將它們復制過來即可。完成后helloworld.exe就可以運行了。其實也可以先設置環境變量,以后在本機就可以直接運行生成的可執行文件了,這個可以參考下面的附錄。
2.編譯release版本程序
可以看到debug版本的可執行文件需要的dll文件是很大的,因為其中包含了調試信息。而我們實際發布軟件是使用的release版本,下面我們就來編譯release版本的helloworld程序。如果前面關閉了Qt Creator,那么需要在Qt Creator中再次打開helloworld項目,可以從歡迎模式的開發頁面中打開最近使用的項目,也可以從開始菜單中打開,還可以將源碼目錄中的.pro文件直接拖入到QtCreator來打開。
然后將版本設置為release版本,也就是發布版本。如下圖所示。完成后運行程序即可。
最后,可以從release目錄中將helloworld.exe復制出來,然后將需要的幾個dll文件(跟debug版本的不是完全一樣哦!)也復制過來,將它們放到一個文件夾中,打包進行發布。
補充:如果要給生成的exe可執行文件更換一個自定義圖標,可以這樣做:
1.在項目中添加一個myapp.rc(名字可以隨意)文件,然后在里面輸入下面一行代碼:
IDI_ICON1 ICON DISCARDABLE "appico.ico"
這里的appico.ico就是自己的.ico圖標文件;
2.在.pro項目文件中添加下面一行代碼:
RC_FILE=myapp.rc
3.重新編譯
四、Qt工具介紹
安裝好Qt后,會在開始菜單生成一個目錄,如下圖所示。
這里是Qt提供的幾個工具軟件。其中Assistant是Qt助手,它已經集成到了Qt Creator中,就是幫助模式;Designer是Qt設計師,它也集成到了QtCreator中,就是設計模式;Exampleand Demos是Qt示例程序和演示程序,其中的演示程序就是一些比較大型的程序,這個我們在歡迎模式已經看到了,不過這里可以直接運行這些程序;Linguist是Qt語言家,是用來對軟件進行國際化翻譯的;下面的Qt 4.8.1 Command Prompt可以用來進行命令行操作,比如使用命名來編譯程序等。
五、附錄
前面為了運行生成的helloworld.exe文件,復制了一些dll文件。其實,如果只想在本機運行程序,那么不必要每次都復制這些文件,只需要將path環境變量設置一下即可。我們在桌面計算機(我的電腦)圖標上點擊鼠標右鍵,選擇屬性,然后選擇高級系統設置,在這里在高級頁面選擇環境變量,然后在系統變量中找到Path變量,雙擊,在變量值的最后,添加上Qt的bin目錄的路徑,我這里是;C:\Qt.8.1\bin(注意,在最前面有個英文半角的分號)。如下圖所示。
這樣以后就不需要再復制那些dll文件了。其實,還有一種方式也不需要dll文件,那就是靜態編譯,不過使用靜態編譯的Qt程序很大,而且不夠靈活,所以這里不再講解,有興趣的朋友可以在網上搜索一下。
這一篇中通過創建一個hello world程序,主要講解了Qt Creator開發環境的創建以及Qt程序運行發布等內容。這一篇是最基本的知識,希望大家先看完本篇再來學習下面的內容。在《Qt Creator快速入門》一書中對開發環境以及hello world程序進行了更加詳細深入的講解,有需要的童鞋可以參考一下。