728x90

PCAN ISOTP API Write 호출 시 비동기 처리가 되기 때문에 loop back을 이용해서 제대로 모든 데이터가 처리가 되었는지 확인할 수 있습니다.

void PCANISOTP::Write(const char *pData, int nSize)
{
    cantp_msg message = {};
    cantp_can_msgtype msgtype = PCANTP_CAN_MSGTYPE_EXTENDED | PCANTP_CAN_MSGTYPE_FD | PCANTP_CAN_MSGTYPE_BRS;

    cantp_netaddrinfo isotp_nai = {};
    isotp_nai.source_addr = N_SA;
    isotp_nai.target_addr = N_TA_PHYS;
    isotp_nai.target_type = PCANTP_ISOTP_ADDRESSING_PHYSICAL;
    isotp_nai.msgtype = PCANTP_ISOTP_MSGTYPE_DIAGNOSTIC;
    isotp_nai.format = PCANTP_ISOTP_FORMAT_NORMAL;

    status = CANTP_MsgDataAlloc_2016(&message, PCANTP_MSGTYPE_ISOTP);
    if (CANTP_StatusIsOk_2016(status)) {
        // initialize ISOTP message
        status = CANTP_MsgDataInit_2016(&message, PCANTP_MAPPING_FLOW_CTRL_NONE, msgtype, nSize, pData, &isotp_nai);
        if (CANTP_StatusIsOk_2016(status)) {
            // write message
            do {
                status = CANTP_Write_2016(channel, &message);
                if (CANTP_StatusIsOk_2016(status)) {
                    debug(QString::asprintf("Successfully queued ISOTP message: Length %i (sts=0x%04X).", nSize, (int)status));
                }
                else {
                    debug(QString::asprintf("Failed to write ISOTP message: Length %i (sts=0x%04X).", nSize, (int)status));
                    return;
                }

            } while ( !checkWriteState(message) );
            emit emit_sendISOTPPacketData(enTx, message);
        }
        else {
            debug(QString::asprintf("Failed to initialize ISOTP message: Length %i (sts=0x%04X).", nSize, (int)status));
            return;
        }
        // release message

        if (!CANTP_StatusIsOk_2016(status)) {
            debug(QString::asprintf("Failed to deallocate message (sts=0x%04X).", status));
            return;
        }
    }
    else {
        debug(QString::asprintf("Failed to allocate message (sts=0x%04X).", status));
        return;
    }
}

bool PCANISOTP::checkWriteState(cantp_msg& rx_msg)
{
    cantp_msg loopback_msg;

    bool bRes = false;
    cantp_msgprogress progress;
    cantp_status result;
    memset(&loopback_msg, 0, sizeof(loopback_msg));
    memset(&progress, 0, sizeof(progress));

    HANDLE receive_event = NULL;

    status = CANTP_GetValue_2016(channel, PCANTP_PARAMETER_RECEIVE_EVENT, &receive_event, sizeof(receive_event));
    if (!CANTP_StatusIsOk_2016(status) || receive_event == NULL) {
        debug(QString("Failed to make loop back receive event"));
        return false;
    }

    do {
        int nResult = WaitForSingleObject(receive_event, 10);
        if (nResult == WAIT_OBJECT_0) {
            status = CANTP_MsgDataAlloc_2016(&loopback_msg, PCANTP_MSGTYPE_NONE);
            // Read transmission confirmation.

            result = CANTP_Read_2016(channel, &loopback_msg, 0, PCANTP_MSGTYPE_ANY);
            if (CANTP_StatusIsOk_2016(result, PCANTP_STATUS_OK, false)) {
                if (((PCANTP_MSGTYPE_ISOTP & loopback_msg.type) == PCANTP_MSGTYPE_ISOTP) && (loopback_msg.msgdata.isotp->flags == PCANTP_MSGFLAG_LOOPBACK)
                    && ((loopback_msg.msgdata.isotp->netaddrinfo.msgtype & PCANTP_ISOTP_MSGTYPE_FLAG_INDICATION_TX) == PCANTP_ISOTP_MSGTYPE_FLAG_INDICATION_TX)) {
                    // The message is being received, wait and show progress
                    do {
                        result = CANTP_GetMsgProgress_2016(channel, &loopback_msg, PCANTP_MSGDIRECTION_RX, &progress);
                    } while (progress.state == PCANTP_MSGPROGRESS_STATE_PROCESSING);

                    if ( CANTP_MsgEqual_2016(&rx_msg, &loopback_msg, true) ) {
                        qDebug() << "Equal";
                        bRes = true;
                    }
                }
            }
        }
    } while ( !bRes );
    CANTP_MsgDataFree_2016(&loopback_msg);

    return bRes;
}

checkWriteState 함수를 통해 loop back read를 진행 후 전송한 cantp_msg를 CANTP_MsgEqual_2016 호출로 비교해서 같다면 전송이 성공했다는 방식으로 처리를 할 수 있습니다.

728x90
728x90

PCAN ISOTP API를 이용해서 데이터를 수신할 때 단순히 Read하는 것이 아니라 ISOTP Read 과정이 모두 완료 되었는지 확인하고 Read를 진행할 수 있는 방법이 있습니다.

int PCANISOTP::read_segmented_message(cantp_handle channel, int &nbMsgRead)
{
    cantp_msg rx_msg;
    cantp_msgprogress progress;     // ISO-TP 메시지가 다 들어왔는지 체크하기 위함

    memset(&progress, 0, sizeof(cantp_msgprogress));
    status = CANTP_Read_2016(channel, &rx_msg, NULL, PCANTP_MSGTYPE_NONE);

    if (CANTP_StatusIsOk_2016(status, PCANTP_STATUS_OK, false) ) {
        if (rx_msg.msgdata.any->flags & PCANTP_MSGFLAG_LOOPBACK) {
            return -1;
        } // loopback message의 경우 pass

        if (((PCANTP_MSGTYPE_ISOTP & rx_msg.type) == PCANTP_MSGTYPE_ISOTP)
            && ((rx_msg.msgdata.isotp->netaddrinfo.msgtype & PCANTP_ISOTP_MSGTYPE_FLAG_INDICATION_RX) == PCANTP_ISOTP_MSGTYPE_FLAG_INDICATION_RX)) {
            // The message is being received, wait and show progress
            do {
                status = CANTP_GetMsgProgress_2016(channel, &rx_msg, PCANTP_MSGDIRECTION_RX, &progress);
            } while (progress.state == PCANTP_MSGPROGRESS_STATE_PROCESSING);

            // The message is received, read it
            status = CANTP_Read_2016(channel, &rx_msg, NULL, PCANTP_MSGTYPE_NONE);
        } // 잔여 데이터가 있을 경우 계속 체크 후 다 들어왔다면 다시 한번 Read
        emit emit_sendISOTPPacketData(enRx, rx_msg);
    }

    return 1;
}

위 코드에서 cantp_msgprogress 구조체를 사용하는데 메시지 처리 상태를 확인할 수 있습니다. 진행 상태, 진행률 등을 볼 수 있으며 이 상태를 체크하면서 while로 state가 PCANTP_MSGPROGRESS_STATE_COMPLETED이 될 때 까지 기다립니다.

728x90
728x90

PEAK CAN ISO-TP API를 사용하면 PC에서도 USB 등을 이용하여 ISO-TP 통신을 사용할 수 있습니다.

 

PCAN-ISO-TP API: PEAK-System

Description ISO-TP (ISO 15765-2) is an international standard for the transfer of data packages via CAN. Above CAN (OSI layers 1 and 2), the protocol covers the OSI layers 3 (Network Layer) and 4 (Transport Layer). It can transmit data packages of up to 4

www.peak-system.com

 

이 API를 사용하면서 가장 헷갈리는 부분 중 하나가 Mapping 함수인데 간단히 설명하려고 합니다.

 

먼저 설명에 앞서 ISO-TP의 사양을 간단히 설명하면 CAN 혹은 CAN-FD의 메시지를 사용해서 통신을 하는 구조입니다. 메시지를 보낼 때 사이즈가 DLC(한번에 보내기로 결정한 데이터 사이즈)보다 크다면 여러 메시지를 주고 받으면서 통신을 진행합니다. 이때 First Frame, ConsecutiveFrame, FlowControl이라는 메시지를 사용하는데 FirstFrame과 Consecutive Frame은 송신 측에서 데이터를 보낼 때 사용하고 FlowControl은 수신측에서 수신 상태를 알려줄 때 사용합니다.

 

이 CAN Message를 보낼 때 ID를 사용하게 되는데 이 때 Mapping으로 ID를 미리 지정해두게 됩니다.

 

만약 내가 이 프로그램에서 ISOTP 메시지를 보낼 때 CAN ID를 0x01로 그리고 상대쪽에서 이 메시지에 대한 FlowControl ID가 0x02라면 아래와 같이 등록합니다. format, msgtype, target_type은 상황에 맞게 설정합니다. Target, Source Address는 Normal Format에서는 크게 중요치 않습니다.

m_SendCANID = 0x01;
m_SendFlowID = 0x02;

status = initSendMsgMappings(channel, m_SendCANID, m_SendFlowID);
if ( !CANTP_StatusIsOk_2016(status) ) {
    debug(QString::asprintf("Failed to Send initialize mapping"));
    return bResult;
} // ISO-TP 주소 매핑
// CAN ID = 내가 쓸 때의 ISOTP CAN ID
// FlowID = 상대방(GUI)에서 응답하는 FlowControl ID
// PCAN-ISO-TP API Usermanual p.265 / ISO-TP Document p.40 참고

cantp_status PCANISOTP::initSendMsgMappings(cantp_handle channel, uint32_t can_id, uint32_t can_id_flow_ctrl)
{
    cantp_mapping mapping_phys_tx;

    // clean variables (it is common to leave can_tx_dl uninitialized which can lead to invalid 0xCC values)
    memset(&mapping_phys_tx, 0, sizeof(mapping_phys_tx));

    // configure a mapping to transmit physical message
    mapping_phys_tx.can_id = can_id;
    mapping_phys_tx.can_id_flow_ctrl = can_id_flow_ctrl;
    mapping_phys_tx.can_msgtype = PCANTP_CAN_MSGTYPE_EXTENDED;
    mapping_phys_tx.netaddrinfo.format = PCANTP_ISOTP_FORMAT_NORMAL;
    mapping_phys_tx.netaddrinfo.msgtype = PCANTP_ISOTP_MSGTYPE_DIAGNOSTIC;
    mapping_phys_tx.netaddrinfo.target_type = PCANTP_ISOTP_ADDRESSING_PHYSICAL;
    mapping_phys_tx.netaddrinfo.source_addr = N_SA;
    mapping_phys_tx.netaddrinfo.target_addr = N_TA_PHYS;
    status = CANTP_AddMapping_2016(channel, &mapping_phys_tx);
    if (!CANTP_StatusIsOk_2016(status)) {
        return status;
    }

    return status;
}

 

Write 할 때 msgDataAlloc 후 MsgDataInit을 하는데 이 때 Mapping 할 때 등록했던 각종 정보를 그대로 전달합니다. 전달받은 netaddrinfo 정보를 토대로 Mapping 된 정보에서 CAN, Flow ID를 가져옵니다.

void PCANISOTP::Write(const char *pData, int nSize)
{
    cantp_msg message = {};
    cantp_can_msgtype msgtype = PCANTP_CAN_MSGTYPE_EXTENDED | PCANTP_CAN_MSGTYPE_FD | PCANTP_CAN_MSGTYPE_BRS;

    cantp_netaddrinfo isotp_nai = {};
    isotp_nai.source_addr = N_SA;
    isotp_nai.target_addr = N_TA_PHYS;
    isotp_nai.target_type = PCANTP_ISOTP_ADDRESSING_PHYSICAL;
    isotp_nai.msgtype = PCANTP_ISOTP_MSGTYPE_DIAGNOSTIC;
    isotp_nai.format = PCANTP_ISOTP_FORMAT_NORMAL;

    status = CANTP_MsgDataAlloc_2016(&message, PCANTP_MSGTYPE_ISOTP);
    if (CANTP_StatusIsOk_2016(status)) {
        // initialize ISOTP message
        status = CANTP_MsgDataInit_2016(&message, PCANTP_MAPPING_FLOW_CTRL_NONE, msgtype, nSize, pData, &isotp_nai);
        ...
}

 

이제 수신쪽 매핑입니다. 만약 상대쪽에서 ISOTP 메시지를 보낼 때 CAN ID를 0x03로 그리고 이 프로그램에서 메시지에 대한 FlowControl ID가 0x04라면 아래와 같이 등록합니다.

m_SendCANID = 0x03;
m_SendFlowID = 0x04;

status = initRecvMsgMappings(channel, m_RecvCANID, m_RecvFlowID);
if ( !CANTP_StatusIsOk_2016(status) ) {
    debug(QString::asprintf("Failed to Recv initialize mapping"));
    return bResult;
} // ISO-TP 주소 매핑
// CAN ID = 상대방의 ISOTP CAN ID
// FlowID = 내가 보내는 FlowControl ID

cantp_status PCANISOTP::initRecvMsgMappings(cantp_handle channel, uint32_t can_id, uint32_t can_id_flow_ctrl)
{
    cantp_mapping mapping_phys_rx;

    // clean variables (it is common to leave can_tx_dl uninitialized which can lead to invalid 0xCC values)
    memset(&mapping_phys_rx, 0, sizeof(mapping_phys_rx));

    // configure a mapping to transmit physical message
    mapping_phys_rx.can_id = can_id;
    mapping_phys_rx.can_id_flow_ctrl = can_id_flow_ctrl;
    mapping_phys_rx.can_msgtype = PCANTP_CAN_MSGTYPE_EXTENDED;
    mapping_phys_rx.netaddrinfo.format = PCANTP_ISOTP_FORMAT_NORMAL;
    mapping_phys_rx.netaddrinfo.msgtype = PCANTP_ISOTP_MSGTYPE_DIAGNOSTIC;
    mapping_phys_rx.netaddrinfo.target_type = PCANTP_ISOTP_ADDRESSING_PHYSICAL;
    mapping_phys_rx.netaddrinfo.source_addr = N_TA_PHYS;
    mapping_phys_rx.netaddrinfo.target_addr = N_SA;
    status = CANTP_AddMapping_2016(channel, &mapping_phys_rx);
    if (!CANTP_StatusIsOk_2016(status)) {
        return status;
    }

    return status;
}

 

자세한 내용은 PCAN ISOTP API 매뉴얼 참고 바랍니다.

728x90
728x90

Qt에서 멀티 쓰레딩을 사용하기 위해서 QThread를 상속받아서 처리하는 예제를 소개한 적이 있습니다.

 

Qt QThread 사용하기(화면 실시간 갱신하기)

센서값을 수집해서 실시간으로 실행창에서 최신화하고 갱신하는 방법에 대해 소개해 드리겠습니다. 먼저 QThread를 상속받는 클래스를 하나 만들어 줍니다. 생성된 qTh.h 파일에 소스코드를 아래

1d1cblog.tistory.com

QtConcurrent를 사용하면 간단하게 비동기 멀티 쓰레딩을 할 수 있습니다. 이를 Qt에서 제공하는 Example을 같이 살펴보며 필요한 부분에 대해 같이 공부해보려 합니다.

 

Example은 Qt Creator > Examples에서 concurrent로 검색하여 첫 번째 예제인 Image Scaling을 사용하려 합니다.

전체 함수를 다 보는 것보다 필요한 부분만 순서대로 보겠습니다. 주석에 나와있는 번호를 따라가면 UI 생성 및 Connect 관계에 대해서는 쉽게 파악이 가능합니다.

 

그리고 중간에 비동기 처리를 확인하기 위해 Debug Message를 출력하는 함수를 추가로 사용하였습니다.

void DebugMsg(const QString& sMsg)
{
    qDebug() << QTime::currentTime().toString("[mm:ss:zzz]") + sMsg;
}

 

[3] 부분인 DownloadDialog를 실행 후 확인을 눌렀을 때 진행되는 부분부터 보겠습니다. DownloadDialog에서는 이미지의 URL들을 관리하고 확인을 눌렀을 때 getUrls를 통해 Url 정보를 가져옵니다.

//! [3]
void Images::process()
{
    // Clean previous state
    replies.clear();
    addUrlsButton->setEnabled(false);

    if (downloadDialog->exec() == QDialog::Accepted) {

        const auto urls = downloadDialog->getUrls();
        if (urls.empty())
            return;

        cancelButton->setEnabled(true);

        initLayout(urls.size());

        downloadFuture = download(urls);
        statusBar->showMessage(tr("Downloading..."));
//! [3]

        //! [4]
        downloadFuture
                .then([this](auto) {
                    cancelButton->setEnabled(false);
                    updateStatus(tr("Scaling..."));
                    //! [16]
                    scalingWatcher.setFuture(QtConcurrent::run(Images::scaled,
                                                               downloadFuture.results()));
                    //! [16]
                })
        //! [4]
        //! [5]
                .onCanceled([this] {
                    updateStatus(tr("Download has been canceled."));
                })
                .onFailed([this](QNetworkReply::NetworkError error) {
                    updateStatus(tr("Download finished with error: %1").arg(error));
                    // Abort all pending requests
                    abortDownload();
                })
                .onFailed([this](const std::exception &ex) {
                    updateStatus(tr(ex.what()));
                })
        //! [5]
                .then([this]() {
                    cancelButton->setEnabled(false);
                    addUrlsButton->setEnabled(true);
                });
    }
}

 

가져온 Url을 Donwload 함수로 넘기게 됩니다.

//! [8]
QFuture<QByteArray> Images::download(const QList<QUrl> &urls)
{
//! [8]
//! [9]
    DebugMsg("DonwloadStart");
    QSharedPointer<QPromise<QByteArray>> promise(new QPromise<QByteArray>());
    promise->start();
//! [9]

    //! [10]
    for (const auto &url : urls) {
        DebugMsg("Donwload " + url.toString());
        QSharedPointer<QNetworkReply> reply(qnam.get(QNetworkRequest(url)));
        replies.push_back(reply);
    //! [10]

    //! [11]
        QtFuture::connect(reply.get(), &QNetworkReply::finished).then([=] {
            if (promise->isCanceled()) {
                if (!promise->future().isFinished())
                    promise->finish();
                return;
            }

            if (reply->error() != QNetworkReply::NoError) {
                if (!promise->future().isFinished())
                    throw reply->error();
            }
        //! [12]
            promise->addResult(reply->readAll());
            DebugMsg("Get Data");

            // Report finished on the last download
            if (promise->future().resultCount() == urls.size()) {
                DebugMsg("Finish promis");
                promise->finish();
            }
        //! [12]
        }).onFailed([promise] (QNetworkReply::NetworkError error) {
            promise->setException(std::make_exception_ptr(error));
            promise->finish();
        }).onFailed([promise] {
            const auto ex = std::make_exception_ptr(
                        std::runtime_error("Unknown error occurred while downloading."));
            promise->setException(ex);
            promise->finish();
        });
    }
    //! [11]

//! [13]
    DebugMsg("Return");
    return promise->future();
}

여기서 QPromise와 QFuture을 사용하게 됩니다. C++에서 Promise와 Furture은 같이 사용하게 되는데 Furture(미래)에 데이터를 돌려주겠다는 Promise(약속)이라고 생각하면 됩니다. QPromise의 비동기 처리를 QFurture에 전달하는 방식입니다. QFurtuer의 Connect를 통해 Signal 처리를 하게 되고 혹은 QFurture::waitforFinish를 통해 비동기 처리가 완료될 때까지 기다릴 수도 있습니다.

 

for문으로 전달받은 Url들을 요청하게 되는데 이에 대한 결과는 QNetworkReply::Finish Signal을 QtFurture::connect로 묶어 처리가 됩니다. 아래 로그를 확인하면 Download 후 바로 Promise가 가지고 있는 QFurture을 Return하게 됩니다. 그 후 요청한 Url에 대한 결과가 도착하고 Url 개수만큼의 완료가 진행 됐다면 QPromise는 종료합니다.

"[23:11:039]DonwloadStart"
"[23:11:039]Donwload https://img1.daumcdn.net/thumb/..."
"[23:11:504]Donwload https://img1.daumcdn.net/thumb/..."
"[23:11:505]Return"
"[23:11:505]Downloading"
"[23:11:724]Get Data"
"[23:11:724]Get Data"
"[23:11:724]Finish promis"

그리고 다시 Process 함수로 돌아와서 QPromise가 finish 됨에 따라 QFurtuer의 then의 lambda 함수가 실행됩니다.

//! [3]
void Images::process()
{
    // Clean previous state
    replies.clear();
    addUrlsButton->setEnabled(false);

    if (downloadDialog->exec() == QDialog::Accepted) {

        const auto urls = downloadDialog->getUrls();
        if (urls.empty())
            return;

        cancelButton->setEnabled(true);

        initLayout(urls.size());

        downloadFuture = download(urls);
        statusBar->showMessage(tr("Downloading..."));
//! [3]

        //! [4]
        downloadFuture
                .then([this](auto) {
                    cancelButton->setEnabled(false);
                    updateStatus(tr("Scaling..."));
                    //! [16]
                    scalingWatcher.setFuture(QtConcurrent::run(Images::scaled,
                                                               downloadFuture.results()));
                    //! [16]
                })
        //! [4]
        //! [5]
                .onCanceled([this] {
                    updateStatus(tr("Download has been canceled."));
                })
                .onFailed([this](QNetworkReply::NetworkError error) {
                    updateStatus(tr("Download finished with error: %1").arg(error));
                    // Abort all pending requests
                    abortDownload();
                })
                .onFailed([this](const std::exception &ex) {
                    updateStatus(tr(ex.what()));
                })
        //! [5]
                .then([this]() {
                    cancelButton->setEnabled(false);
                    addUrlsButton->setEnabled(true);
                });
    }
}

여기서 QFurtherWatcher라는 객체에 QtConcurrent::run의 Return을 넘겨주게 됩니다. QFurtherWatcher는 QFurture에 대한 상태를 체크하고 QFurture가 Singal 상태가 됐을 때 실행할 Slot을 connect 시켜주었습니다.

Images::Images(QWidget *parent) : QWidget(parent), downloadDialog(new DownloadDialog(this))
{
    resize(800, 600);

    ...

//! [6]
    connect(&scalingWatcher, &QFutureWatcher<QList<QImage>>::finished, this, &Images::scaleFinished);
//! [6]
}

//! [15]
void Images::scaleFinished()
{
    DebugMsg("Sacle Finished");
    const OptionalImages result = scalingWatcher.result();
    if (result.has_value()) {
        const auto scaled = result.value();
        showImages(scaled);
        updateStatus(tr("Finished"));
    } else {
        updateStatus(tr("Failed to extract image data."));
    }
    addUrlsButton->setEnabled(true);
}

최종적으로 실행된 로그를 보면 아래와 같습니다.

"[23:11:039]DonwloadStart"
"[23:11:039]Donwload https://img1.daumcdn.net/thumb/..."
"[23:11:504]Donwload https://img1.daumcdn.net/thumb/..."
"[23:11:505]Return"
"[23:11:505]Downloading"
"[23:11:724]Get Data"
"[23:11:724]Get Data"
"[23:11:724]Finish promis"
"[23:11:724]Scaling"
"[23:11:724]Scale Image"
"[23:11:752]Sacle Finished"

정리해보자면 QThread를 사용하지 않으면서 비동기 처리를 위해선 QFurture, QPromise 등을 사용하면서 종료를 대기 혹은 신호를 받거나 혹은 QFurtureWatcher에 QFurture를 등록 후 사용도 가능합니다.

 

QFurture를 간단히 사용하기 위해서는 Qt::Concorrent::run을 통해 함수 포인터와 인자를 넘겨줌으로써 그 처리의 QFurture를 반환받아 위처럼 사용도 가능합니다.

.

728x90
728x90

QProcess를 이용해서 cmd 명령어를 실행 후 결과를 얻어오는 코드입니다.

QProcess *m_process = new QProcess(this);

QString program = "ipconfig";
QStringList arguments;

arguments << "/all";

m_process->start(program, arguments);
m_process->waitForFinished(3000);

QByteArray result = m_process->readAll();
auto converter = QStringDecoder(QStringDecoder::System);
QString string = converter(result);
qDebug() << string;

m_process->close();
delete m_process;
728x90
728x90

100mb가 넘는 파일을 push 할 때 remote: GitLab: You are attempting to check in one or more blobs which exceed the 100.0MiB limit: 오류가 뜨는 경우가 있습니다. 이럴 경우에는 git lfs를 설정해주어야 합니다.

 

먼저 https://git-lfs.com/에서 Download 해줍니다.

 

Git Large File Storage

Git Large File Storage (LFS) replaces large files such as audio samples, videos, datasets, and graphics with text pointers inside Git, while storing the file contents on a remote server like GitHub.com or GitHub Enterprise.

git-lfs.com

다 설치가 되었으면 해당 파일 경로로 이동해 아래 명령어를 입력합니다.

git lfs track "file.txt"

그러면 .gitatrribute라는 파일이 생기고 이제 push하면 제대로 업로드가 됩니다.

728x90

'Programming > Git' 카테고리의 다른 글

git revert  (0) 2020.06.04
git reset  (0) 2020.06.04
git checkout  (0) 2020.05.24
git diff  (0) 2020.05.24
git add, git commit, git status, git log  (0) 2020.05.24
728x90

Combobox에 따라 Layout에 있는 Label을 바꾸려고 합니다. Combobox에 index가 바뀌면 replaceWidget 함수를 호출하도록 작성했습니다.

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    QLabel* pLabel = new QLabel("test1");
    QLabel* pLabel2 = new QLabel("test2");

    labelList.append(pLabel);
    labelList.append(pLabel2);

    ui->comboBox->addItem(pLabel->text(), 0);
    ui->comboBox->addItem(pLabel2->text(),1);

    ui->verticalLayout->addWidget(pLabel);
    pCurrent = pLabel;
    connect(ui->comboBox, &QComboBox::currentIndexChanged, [&] (int index) {
        auto label = labelList.value(index);
        ui->verticalLayout->replaceWidget(pCurrent, label);
        pCurrent = label;
    });
}

그런데 실제로 Replace 해보면 이전의 Widget이 남아 있습니다.

이럴 경우에는 2가지 해결 방법이 있습니다. 이전의 widget을 delete 시키는 방법과 기존 parent를 없애는 방법입니다.

하지만 delete를 하는 방식의 큰 문제는 위와 같이 교체된 widget도 나중에 사용해야 하는 경우에는 맞지 않습니다. 이럴 때는 교체된 widget의 Parent를 0으로 넘겨주면 됩니다.

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    QLabel* pLabel = new QLabel("test1");
    QLabel* pLabel2 = new QLabel("test2");

    labelList.append(pLabel);
    labelList.append(pLabel2);

    ui->comboBox->addItem(pLabel->text(), 0);
    ui->comboBox->addItem(pLabel2->text(),1);

    ui->verticalLayout->addWidget(pLabel);
    pCurrent = pLabel;
    connect(ui->comboBox, &QComboBox::currentIndexChanged, [&] (int index) {
        auto label = labelList.value(index);
        ui->verticalLayout->replaceWidget(pCurrent, label);
        pCurrent->setParent(0);
        pCurrent = label;
    });
}

728x90
728x90

Qt를 설치할 때 볼 수 있던 아래와 같은 구조가지는 UI Component를 TreeWidget이라고 합니다. Qt에서 TreeWidget을 사용하는 법에 대해 간단히 소개하려고 합니다.

먼저 UI는 아래와 같이 구성했습니다. 상단에 QTreeWidget 그리고 하단에는 선택한 Item의 Text를 확인할 수 있는 Label과 선택된 Item에 자식 Item을 추가할 수 있는 Insert와 해당 Item을 지울 Delete 버튼입니다.

먼저 간단하게 Header와 몇가지 Item을 추가해보겠습니다.

#include "mainwindow.h"
#include "./ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    QTreeWidgetItem* headerItem = new QTreeWidgetItem;
    headerItem->setText(0, "Name");
    headerItem->setText(1, "Price");
    ui->treeWidget->setHeaderItem(headerItem);  // header

    QTreeWidgetItem* Parent = new QTreeWidgetItem(ui->treeWidget);
    Parent->setText(0, "Nintendo");

    QTreeWidgetItem* Child = new QTreeWidgetItem(Parent);
    Child->setText(0, "Zelda");
    Child->setText(1, "70000");
}

MainWindow::~MainWindow()
{
    delete ui;
}

Nintendo라는 부모 Item 아래 Zelda 라는 자식 Item을 추가했습니다.

여기에 선택된 Item의 Name을 label에 보여주는 Connect를 추가해보겠습니다.

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    QTreeWidgetItem* headerItem = new QTreeWidgetItem;
    headerItem->setText(0, "Name");
    headerItem->setText(1, "Price");
    ui->treeWidget->setHeaderItem(headerItem);  // header

    QTreeWidgetItem* Parent = new QTreeWidgetItem(ui->treeWidget);
    Parent->setText(0, "Nintendo");

    QTreeWidgetItem* Child = new QTreeWidgetItem(Parent);
    Child->setText(0, "Zelda");
    Child->setText(1, "70000");

    connect(ui->treeWidget, &QTreeWidget::itemClicked, [&] (QTreeWidgetItem *item, int column) {
        QString sName = item->text(0);
        ui->lbTreeItemTitle->setText(sName);
    });
}

다음으로 Add Child Item, Delete Item 코드입니다.

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    QTreeWidgetItem* headerItem = new QTreeWidgetItem;
    headerItem->setText(0, "Name");
    headerItem->setText(1, "Price");
    ui->treeWidget->setHeaderItem(headerItem);  // header

    QTreeWidgetItem* Parent = new QTreeWidgetItem(ui->treeWidget);
    Parent->setText(0, "Nintendo");

    QTreeWidgetItem* Child = new QTreeWidgetItem(Parent);
    Child->setText(0, "Zelda");
    Child->setText(1, "70000");

    connect(ui->treeWidget, &QTreeWidget::itemClicked, [&] (QTreeWidgetItem *item, int column) {
        QString sName = item->text(0);
        ui->lbTreeItemTitle->setText(sName);
    }); // Display Current Selected Item Name(Column = 0)

    connect(ui->pb_AddChild, &QPushButton::clicked, [&] {
        QTreeWidgetItem* pCurrentSelectedItem = ui->treeWidget->currentItem();
        if ( pCurrentSelectedItem == nullptr ) { return; }

        QTreeWidgetItem* pNewChild = new QTreeWidgetItem;
        pNewChild->setText(0, ui->le_ChildTitle->text());
        pCurrentSelectedItem->addChild(pNewChild);
    }); // Add Child Item

    connect(ui->pb_DeleteItem, &QPushButton::clicked, [&] {
        QTreeWidgetItem* pCurrentSelectedItem = ui->treeWidget->currentItem();
        if ( pCurrentSelectedItem == nullptr ) { return; }

        delete pCurrentSelectedItem;
    }); // Delete Current Item
}

다음은 Item을 접고 펼칠때마다 Column의 크기를 재 조정할 수 있는 기능입니다. 

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    QTreeWidgetItem* headerItem = new QTreeWidgetItem;
    headerItem->setText(0, "Name");
    headerItem->setText(1, "Price");
    ui->treeWidget->setHeaderItem(headerItem);  // header

    QTreeWidgetItem* Parent = new QTreeWidgetItem(ui->treeWidget);
    Parent->setText(0, "Nintendo");

    QTreeWidgetItem* Child = new QTreeWidgetItem(Parent);
    Child->setText(0, "Zelda");
    Child->setText(1, "70000");

    connect(ui->treeWidget, &QTreeWidget::itemClicked, [&] (QTreeWidgetItem *item, int column) {
        QString sName = item->text(0);
        ui->lbTreeItemTitle->setText(sName);
    }); // Display Current Selected Item Name(Column = 0)

    connect(ui->pb_AddChild, &QPushButton::clicked, [&] {
        QTreeWidgetItem* pCurrentSelectedItem = ui->treeWidget->currentItem();
        if ( pCurrentSelectedItem == nullptr ) { return; }

        QTreeWidgetItem* pNewChild = new QTreeWidgetItem;
        pNewChild->setText(0, ui->le_ChildTitle->text());
        pCurrentSelectedItem->addChild(pNewChild);
    }); // Add Child Item

    connect(ui->pb_DeleteItem, &QPushButton::clicked, [&] {
        QTreeWidgetItem* pCurrentSelectedItem = ui->treeWidget->currentItem();
        if ( pCurrentSelectedItem == nullptr ) { return; }

        delete pCurrentSelectedItem;
    }); // Delete Current Item

    connect(ui->treeWidget, &QTreeWidget::itemExpanded, [&] (QTreeWidgetItem *item) {
        ui->treeWidget->resizeColumnToContents(0);
        ui->treeWidget->resizeColumnToContents(1);
    }); // Resize Columns

    connect(ui->treeWidget, &QTreeWidget::itemCollapsed, [&] (QTreeWidgetItem *item) {
        ui->treeWidget->resizeColumnToContents(0);
        ui->treeWidget->resizeColumnToContents(1);
    }); // Resize Columns
}

마지막으로 TreeWidgetItem에 체크박스를 추가하고 체크 상태에 따라 부모와 자식의 상태도 자동으로 변하는 기능입니다.

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    QTreeWidgetItem* headerItem = new QTreeWidgetItem;
    headerItem->setText(0, "Name");
    headerItem->setText(1, "Price");
    ui->treeWidget->setHeaderItem(headerItem);  // header

    QTreeWidgetItem* Parent = new QTreeWidgetItem(ui->treeWidget);
    Parent->setCheckState(0, Qt::Unchecked);
    Parent->setFlags(Parent->flags() | Qt::ItemIsAutoTristate | Qt::ItemIsUserCheckable );
    Parent->setText(0, "Nintendo");

    QTreeWidgetItem* Child = new QTreeWidgetItem(Parent);
    Child->setFlags(Child->flags() | Qt::ItemIsUserCheckable );
    Child->setCheckState(0, Qt::Unchecked);
    Child->setText(0, "Zelda");
    Child->setText(1, "70000");

    Child = new QTreeWidgetItem(Parent);
    Child->setFlags(Child->flags() | Qt::ItemIsUserCheckable );
    Child->setCheckState(0, Qt::Unchecked);
    Child->setText(0, "Mario");
    Child->setText(1, "75000");

    Child = new QTreeWidgetItem(Parent);
    Child->setFlags(Child->flags() | Qt::ItemIsUserCheckable );
    Child->setCheckState(0, Qt::Unchecked);
    Child->setText(0, "Pockemon");
    Child->setText(1, "60000");

    connect(ui->treeWidget, &QTreeWidget::itemClicked, [&] (QTreeWidgetItem *item, int column) {
        QString sName = item->text(0);
        ui->lbTreeItemTitle->setText(sName);
    }); // Display Current Selected Item Name(Column = 0)

    connect(ui->pb_AddChild, &QPushButton::clicked, [&] {
        QTreeWidgetItem* pCurrentSelectedItem = ui->treeWidget->currentItem();
        if ( pCurrentSelectedItem == nullptr ) { return; }

        QTreeWidgetItem* pNewChild = new QTreeWidgetItem;
        pNewChild->setText(0, ui->le_ChildTitle->text());
        pCurrentSelectedItem->addChild(pNewChild);
    }); // Add Child Item

    connect(ui->pb_DeleteItem, &QPushButton::clicked, [&] {
        QTreeWidgetItem* pCurrentSelectedItem = ui->treeWidget->currentItem();
        if ( pCurrentSelectedItem == nullptr ) { return; }

        delete pCurrentSelectedItem;
    }); // Delete Current Item

    connect(ui->treeWidget, &QTreeWidget::itemExpanded, [&] (QTreeWidgetItem *item) {
        ui->treeWidget->resizeColumnToContents(0);
        ui->treeWidget->resizeColumnToContents(1);
    }); // Resize Columns

    connect(ui->treeWidget, &QTreeWidget::itemCollapsed, [&] (QTreeWidgetItem *item) {
        ui->treeWidget->resizeColumnToContents(0);
        ui->treeWidget->resizeColumnToContents(1);
    }); // Resize Columns
}

자식 Item에 Check 시에는 자식이 다수일 경우에는 Qt::PartiallyChecked이고 다 선택되면 Qt::Checked 상태 그리고 부모에서 Check 시에는 자식 Item이 모두 Qt::Checked 상태로 변하게 됩니다.

만약에 자식 item에는 checkbox를 두지 않고 싶으면 부모에 Qt::ItemIsAutoTristate는 제거하면 됩니다. 

728x90

+ Recent posts